From 68d54dfdaf523bf321cb86eaa8a73449df22ace5 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 11 Feb 2025 16:57:51 +1100 Subject: [PATCH 01/80] initial Sonic Curve AMO deploy script --- contracts/contracts/proxies/SonicProxies.sol | 7 ++ .../strategies/BaseCurveAMOStrategy.sol | 11 ++- .../sonic/SonicCurveAMOStrategy.sol | 30 ++++++ .../contracts/vault/OSonicVaultAdmin.sol | 39 ++++++++ contracts/deploy/base/025_base_curve_amo.js | 2 + contracts/deploy/sonic/009_curve_amo.js | 98 +++++++++++++++++++ contracts/utils/addresses.js | 8 ++ 7 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol create mode 100644 contracts/deploy/sonic/009_curve_amo.js diff --git a/contracts/contracts/proxies/SonicProxies.sol b/contracts/contracts/proxies/SonicProxies.sol index f4c721c08a..afce5699c7 100644 --- a/contracts/contracts/proxies/SonicProxies.sol +++ b/contracts/contracts/proxies/SonicProxies.sol @@ -44,3 +44,10 @@ contract SonicStakingStrategyProxy is InitializeGovernedUpgradeabilityProxy { contract OSonicHarvesterProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice SonicCurveAMOStrategyProxy delegates calls to a SonicCurveAMOStrategy implementation + */ +contract SonicCurveAMOStrategyProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/contracts/strategies/BaseCurveAMOStrategy.sol b/contracts/contracts/strategies/BaseCurveAMOStrategy.sol index dd6910a161..fd14703f82 100644 --- a/contracts/contracts/strategies/BaseCurveAMOStrategy.sol +++ b/contracts/contracts/strategies/BaseCurveAMOStrategy.sol @@ -60,8 +60,8 @@ contract BaseCurveAMOStrategy is InitializableAbstractStrategy { IChildLiquidityGaugeFactory public immutable gaugeFactory; // Ordered list of pool assets - uint128 public constant oethCoinIndex = 1; - uint128 public constant wethCoinIndex = 0; + uint128 public immutable oethCoinIndex; + uint128 public immutable wethCoinIndex; /** * @notice Maximum slippage allowed for adding/removing liquidity from the Curve pool. @@ -124,8 +124,13 @@ contract BaseCurveAMOStrategy is InitializableAbstractStrategy { address _oeth, address _weth, address _gauge, - address _gaugeFactory + address _gaugeFactory, + uint128 _oethCoinIndex, + uint128 _wethCoinIndex ) InitializableAbstractStrategy(_baseConfig) { + oethCoinIndex = _oethCoinIndex; + wethCoinIndex = _wethCoinIndex; + lpToken = IERC20(_baseConfig.platformAddress); curvePool = ICurveStableSwapNG(_baseConfig.platformAddress); diff --git a/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol new file mode 100644 index 0000000000..60a8f93e31 --- /dev/null +++ b/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { BaseCurveAMOStrategy } from "../BaseCurveAMOStrategy.sol"; + +/** + * @title Curve AMO Strategy for the Origin Sonic Vault + * @author Origin Protocol Inc + */ +contract SonicCurveAMOStrategy is BaseCurveAMOStrategy { + constructor( + BaseStrategyConfig memory _baseConfig, + address _os, + address _ws, + address _gauge, + address _gaugeFactory, + uint128 _osCoinIndex, + uint128 _wsCoinIndex + ) + BaseCurveAMOStrategy( + _baseConfig, + _os, + _ws, + _gauge, + _gaugeFactory, + _osCoinIndex, + _wsCoinIndex + ) + {} +} diff --git a/contracts/contracts/vault/OSonicVaultAdmin.sol b/contracts/contracts/vault/OSonicVaultAdmin.sol index 11cb133a43..ae5cf8c2c4 100644 --- a/contracts/contracts/vault/OSonicVaultAdmin.sol +++ b/contracts/contracts/vault/OSonicVaultAdmin.sol @@ -40,4 +40,43 @@ contract OSonicVaultAdmin is OETHVaultAdmin { emit AssetSupported(_asset); } + + /** + * @notice Adds a strategy to the mint whitelist. + * Reverts if strategy isn't approved on Vault. + * @param strategyAddr Strategy address + */ + function addStrategyToMintWhitelist(address strategyAddr) + external + onlyGovernor + { + require(strategies[strategyAddr].isSupported, "Strategy not approved"); + + require( + !isMintWhitelistedStrategy[strategyAddr], + "Already whitelisted" + ); + + isMintWhitelistedStrategy[strategyAddr] = true; + + emit StrategyAddedToMintWhitelist(strategistAddr); + } + + /** + * @notice Removes a strategy from the mint whitelist. + * @param strategyAddr Strategy address + */ + function removeStrategyFromMintWhitelist(address strategyAddr) + external + onlyGovernor + { + // Intentionally skipping `strategies.isSupported` check since + // we may wanna remove an address even after removing the strategy + + require(isMintWhitelistedStrategy[strategyAddr], "Not whitelisted"); + + isMintWhitelistedStrategy[strategyAddr] = false; + + emit StrategyRemovedFromMintWhitelist(strategistAddr); + } } diff --git a/contracts/deploy/base/025_base_curve_amo.js b/contracts/deploy/base/025_base_curve_amo.js index 3e8832d518..456931688b 100644 --- a/contracts/deploy/base/025_base_curve_amo.js +++ b/contracts/deploy/base/025_base_curve_amo.js @@ -40,6 +40,8 @@ module.exports = deployOnBase( addresses.base.WETH, addresses.base.OETHb_WETH.gauge, addresses.base.childLiquidityGaugeFactory, + 1, // SuperOETH is coin 1 of the Curve WETH/SuperOETH pool + 0, // WETH is coin 0 of the Curve WETH/SuperOETH pool ] ); const cOETHBaseCurveAMO = await ethers.getContractAt( diff --git a/contracts/deploy/sonic/009_curve_amo.js b/contracts/deploy/sonic/009_curve_amo.js new file mode 100644 index 0000000000..d1d4ef58fb --- /dev/null +++ b/contracts/deploy/sonic/009_curve_amo.js @@ -0,0 +1,98 @@ +const { deployOnSonic } = require("../../utils/deploy-l2"); +const { + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); +const { oethUnits } = require("../../test/helpers"); + +module.exports = deployOnSonic( + { + deployName: "009_curve_amo", + }, + async ({ ethers }) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // Deploy Sonic Curve AMO Strategy proxy + const cOSonicProxy = await ethers.getContract("OSonicProxy"); + const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); + const cOSonicVaultAdmin = await ethers.getContractAt( + "OSonicVaultAdmin", + cOSonicVaultProxy.address + ); + + const dSonicCurveAMOStrategyProxy = await deployWithConfirmation( + "SonicCurveAMOStrategyProxy", + [] + ); + + const cSonicCurveAMOStrategyProxy = await ethers.getContract( + "SonicCurveAMOStrategyProxy" + ); + + // Deploy Sonic Curve AMO Strategy implementation + const dSonicCurveAMOStrategy = await deployWithConfirmation( + "SonicCurveAMOStrategy", + [ + [addresses.sonic.WS_OS.pool, cOSonicVaultProxy.address], + cOSonicProxy.address, + addresses.sonic.wS, + addresses.sonic.WS_OS.gauge, + addresses.sonic.childLiquidityGaugeFactory, + 0, // The OToken (OS for Sonic) is coin 0 of the Curve OS/wS pool + 1, // The WETH token (wS for Sonic) is coin 1 of the Curve OS/wS pool + ] + ); + const cSonicCurveAMOStrategy = await ethers.getContractAt( + "SonicCurveAMOStrategy", + dSonicCurveAMOStrategyProxy.address + ); + + // Initialize Sonic Curve AMO Strategy implementation + const initData = cSonicCurveAMOStrategy.interface.encodeFunctionData( + "initialize(address[],uint256)", + [[addresses.sonic.CRV], oethUnits("0.002")] + ); + await withConfirmation( + // prettier-ignore + cSonicCurveAMOStrategyProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dSonicCurveAMOStrategy.address, + addresses.sonic.timelock, + initData + ) + ); + + // Deploy a new Vault Admin implementation + const dOSonicVaultAdmin = await deployWithConfirmation("OSonicVaultAdmin", [ + addresses.sonic.wS, + ]); + console.log( + `Deployed Origin Sonic Vault Admin to ${dOSonicVaultAdmin.address}` + ); + + return { + actions: [ + // 1. Upgrade the VaultAdmin + { + contract: cOSonicVaultAdmin, + signature: "setAdminImpl(address)", + args: [dOSonicVaultAdmin.address], + }, + // 2. Approve strategy on vault + { + contract: cOSonicVaultAdmin, + signature: "approveStrategy(address)", + args: [cSonicCurveAMOStrategyProxy.address], + }, + // 3. Add strategy to mint whitelist + { + contract: cOSonicVaultAdmin, + signature: "addStrategyToMintWhitelist(address)", + args: [cSonicCurveAMOStrategyProxy.address], + }, + ], + }; + } +); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 869fd2af02..cb39226a3b 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -392,6 +392,14 @@ addresses.sonic.SwapXOsUSDCeMultisigBooster = addresses.sonic.SwapXOsGEMSxMultisigBooster = "0xE2c01Cc951E8322992673Fa2302054375636F7DE"; +// Curve +addresses.sonic.CRV = "0x5Af79133999f7908953E94b7A5CF367740Ebee35"; +addresses.sonic.WS_OS = {}; +addresses.sonic.WS_OS.pool = "0x7180f41a71f13fac52d2cfb17911f5810c8b0bb9"; +addresses.sonic.WS_OS.gauge = "0x9ca6de419e9fc7bac876de07f0f6ec96331ba207"; +addresses.sonic.childLiquidityGaugeFactory = + "0xf3A431008396df8A8b2DF492C913706BDB0874ef"; + // Holesky addresses.holesky.WETH = "0x94373a4919B3240D86eA41593D5eBa789FEF3848"; From 8b2b94de6c5ec4c72500e39751a1172121301d7a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 12 Feb 2025 12:56:29 +1100 Subject: [PATCH 02/80] Set governor to zero address on Vault implementation contracts --- contracts/contracts/vault/OETHVaultAdmin.sol | 3 +++ contracts/contracts/vault/OETHVaultCore.sol | 3 +++ 2 files changed, 6 insertions(+) diff --git a/contracts/contracts/vault/OETHVaultAdmin.sol b/contracts/contracts/vault/OETHVaultAdmin.sol index 009a7eec5c..04596d03a8 100644 --- a/contracts/contracts/vault/OETHVaultAdmin.sol +++ b/contracts/contracts/vault/OETHVaultAdmin.sol @@ -19,6 +19,9 @@ contract OETHVaultAdmin is VaultAdmin { constructor(address _weth) { weth = _weth; + + // prevent implementation contract to be governed + _setGovernor(address(0)); } /// @dev Simplified version of the deposit function as WETH is the only supported asset. diff --git a/contracts/contracts/vault/OETHVaultCore.sol b/contracts/contracts/vault/OETHVaultCore.sol index d907192232..09ea2d2c3c 100644 --- a/contracts/contracts/vault/OETHVaultCore.sol +++ b/contracts/contracts/vault/OETHVaultCore.sol @@ -26,6 +26,9 @@ contract OETHVaultCore is VaultCore { constructor(address _weth) { weth = _weth; + + // prevent implementation contract to be governed + _setGovernor(address(0)); } /** From 986abf826e2a6ea67c6fa80b1ea4e7a16492383f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 12 Feb 2025 12:58:11 +1100 Subject: [PATCH 03/80] Base OETH Vault to get full async withdrawal support --- contracts/contracts/vault/OETHBaseVaultAdmin.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/vault/OETHBaseVaultAdmin.sol b/contracts/contracts/vault/OETHBaseVaultAdmin.sol index b5020fb5c1..152135742b 100644 --- a/contracts/contracts/vault/OETHBaseVaultAdmin.sol +++ b/contracts/contracts/vault/OETHBaseVaultAdmin.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import { VaultAdmin } from "./VaultAdmin.sol"; +import { OETHVaultAdmin } from "./OETHVaultAdmin.sol"; /** * @title OETH Base VaultAdmin Contract * @author Origin Protocol Inc */ -contract OETHBaseVaultAdmin is VaultAdmin { +contract OETHBaseVaultAdmin is OETHVaultAdmin { + constructor(address _weth) OETHVaultAdmin(_weth) {} + /** * @notice Adds a strategy to the mint whitelist. * Reverts if strategy isn't approved on Vault. From 20375e672550e6afb082a8f359eb6aef8b1e9397 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 12 Feb 2025 17:35:28 +1100 Subject: [PATCH 04/80] Moved support for multi AMOs up to the OETH Vault --- .../contracts/vault/OETHBaseVaultAdmin.sol | 39 ----------------- .../contracts/vault/OETHBaseVaultCore.sol | 42 ------------------- contracts/contracts/vault/OETHVaultAdmin.sol | 39 +++++++++++++++++ contracts/contracts/vault/OETHVaultCore.sol | 42 +++++++++++++++++++ .../contracts/vault/OSonicVaultAdmin.sol | 39 ----------------- contracts/deploy/sonic/009_curve_amo.js | 18 ++++++-- contracts/utils/addresses.js | 4 +- 7 files changed, 98 insertions(+), 125 deletions(-) diff --git a/contracts/contracts/vault/OETHBaseVaultAdmin.sol b/contracts/contracts/vault/OETHBaseVaultAdmin.sol index 152135742b..34124d79d9 100644 --- a/contracts/contracts/vault/OETHBaseVaultAdmin.sol +++ b/contracts/contracts/vault/OETHBaseVaultAdmin.sol @@ -9,43 +9,4 @@ import { OETHVaultAdmin } from "./OETHVaultAdmin.sol"; */ contract OETHBaseVaultAdmin is OETHVaultAdmin { constructor(address _weth) OETHVaultAdmin(_weth) {} - - /** - * @notice Adds a strategy to the mint whitelist. - * Reverts if strategy isn't approved on Vault. - * @param strategyAddr Strategy address - */ - function addStrategyToMintWhitelist(address strategyAddr) - external - onlyGovernor - { - require(strategies[strategyAddr].isSupported, "Strategy not approved"); - - require( - !isMintWhitelistedStrategy[strategyAddr], - "Already whitelisted" - ); - - isMintWhitelistedStrategy[strategyAddr] = true; - - emit StrategyAddedToMintWhitelist(strategistAddr); - } - - /** - * @notice Removes a strategy from the mint whitelist. - * @param strategyAddr Strategy address - */ - function removeStrategyFromMintWhitelist(address strategyAddr) - external - onlyGovernor - { - // Intentionally skipping `strategies.isSupported` check since - // we may wanna remove an address even after removing the strategy - - require(isMintWhitelistedStrategy[strategyAddr], "Not whitelisted"); - - isMintWhitelistedStrategy[strategyAddr] = false; - - emit StrategyRemovedFromMintWhitelist(strategistAddr); - } } diff --git a/contracts/contracts/vault/OETHBaseVaultCore.sol b/contracts/contracts/vault/OETHBaseVaultCore.sol index 1dd368e413..eb9299e81a 100644 --- a/contracts/contracts/vault/OETHBaseVaultCore.sol +++ b/contracts/contracts/vault/OETHBaseVaultCore.sol @@ -18,48 +18,6 @@ contract OETHBaseVaultCore is OETHVaultCore { constructor(address _weth) OETHVaultCore(_weth) {} - // @inheritdoc VaultCore - function mintForStrategy(uint256 amount) - external - override - whenNotCapitalPaused - { - require( - strategies[msg.sender].isSupported == true, - "Unsupported strategy" - ); - require( - isMintWhitelistedStrategy[msg.sender] == true, - "Not whitelisted strategy" - ); - - emit Mint(msg.sender, amount); - - // Mint matching amount of OTokens - oUSD.mint(msg.sender, amount); - } - - // @inheritdoc VaultCore - function burnForStrategy(uint256 amount) - external - override - whenNotCapitalPaused - { - require( - strategies[msg.sender].isSupported == true, - "Unsupported strategy" - ); - require( - isMintWhitelistedStrategy[msg.sender] == true, - "Not whitelisted strategy" - ); - - emit Redeem(msg.sender, amount); - - // Burn OTokens - oUSD.burn(msg.sender, amount); - } - // @inheritdoc OETHVaultCore function _redeem(uint256 _amount, uint256 _minimumUnitAmount) internal diff --git a/contracts/contracts/vault/OETHVaultAdmin.sol b/contracts/contracts/vault/OETHVaultAdmin.sol index 04596d03a8..cf937b47d7 100644 --- a/contracts/contracts/vault/OETHVaultAdmin.sol +++ b/contracts/contracts/vault/OETHVaultAdmin.sol @@ -24,6 +24,45 @@ contract OETHVaultAdmin is VaultAdmin { _setGovernor(address(0)); } + /** + * @notice Adds a strategy to the mint whitelist. + * Reverts if strategy isn't approved on Vault. + * @param strategyAddr Strategy address + */ + function addStrategyToMintWhitelist(address strategyAddr) + external + onlyGovernor + { + require(strategies[strategyAddr].isSupported, "Strategy not approved"); + + require( + !isMintWhitelistedStrategy[strategyAddr], + "Already whitelisted" + ); + + isMintWhitelistedStrategy[strategyAddr] = true; + + emit StrategyAddedToMintWhitelist(strategistAddr); + } + + /** + * @notice Removes a strategy from the mint whitelist. + * @param strategyAddr Strategy address + */ + function removeStrategyFromMintWhitelist(address strategyAddr) + external + onlyGovernor + { + // Intentionally skipping `strategies.isSupported` check since + // we may wanna remove an address even after removing the strategy + + require(isMintWhitelistedStrategy[strategyAddr], "Not whitelisted"); + + isMintWhitelistedStrategy[strategyAddr] = false; + + emit StrategyRemovedFromMintWhitelist(strategistAddr); + } + /// @dev Simplified version of the deposit function as WETH is the only supported asset. function _depositToStrategy( address _strategyToAddress, diff --git a/contracts/contracts/vault/OETHVaultCore.sol b/contracts/contracts/vault/OETHVaultCore.sol index 09ea2d2c3c..ac6701306b 100644 --- a/contracts/contracts/vault/OETHVaultCore.sol +++ b/contracts/contracts/vault/OETHVaultCore.sol @@ -47,6 +47,48 @@ contract OETHVaultCore is VaultCore { require(allAssets[wethAssetIndex] == weth, "Invalid WETH Asset Index"); } + // @inheritdoc VaultCore + function mintForStrategy(uint256 amount) + external + override + whenNotCapitalPaused + { + require( + strategies[msg.sender].isSupported == true, + "Unsupported strategy" + ); + require( + isMintWhitelistedStrategy[msg.sender] == true, + "Not whitelisted strategy" + ); + + emit Mint(msg.sender, amount); + + // Mint matching amount of OTokens + oUSD.mint(msg.sender, amount); + } + + // @inheritdoc VaultCore + function burnForStrategy(uint256 amount) + external + override + whenNotCapitalPaused + { + require( + strategies[msg.sender].isSupported == true, + "Unsupported strategy" + ); + require( + isMintWhitelistedStrategy[msg.sender] == true, + "Not whitelisted strategy" + ); + + emit Redeem(msg.sender, amount); + + // Burn OTokens + oUSD.burn(msg.sender, amount); + } + // @inheritdoc VaultCore // slither-disable-start reentrancy-no-eth function _mint( diff --git a/contracts/contracts/vault/OSonicVaultAdmin.sol b/contracts/contracts/vault/OSonicVaultAdmin.sol index ae5cf8c2c4..11cb133a43 100644 --- a/contracts/contracts/vault/OSonicVaultAdmin.sol +++ b/contracts/contracts/vault/OSonicVaultAdmin.sol @@ -40,43 +40,4 @@ contract OSonicVaultAdmin is OETHVaultAdmin { emit AssetSupported(_asset); } - - /** - * @notice Adds a strategy to the mint whitelist. - * Reverts if strategy isn't approved on Vault. - * @param strategyAddr Strategy address - */ - function addStrategyToMintWhitelist(address strategyAddr) - external - onlyGovernor - { - require(strategies[strategyAddr].isSupported, "Strategy not approved"); - - require( - !isMintWhitelistedStrategy[strategyAddr], - "Already whitelisted" - ); - - isMintWhitelistedStrategy[strategyAddr] = true; - - emit StrategyAddedToMintWhitelist(strategistAddr); - } - - /** - * @notice Removes a strategy from the mint whitelist. - * @param strategyAddr Strategy address - */ - function removeStrategyFromMintWhitelist(address strategyAddr) - external - onlyGovernor - { - // Intentionally skipping `strategies.isSupported` check since - // we may wanna remove an address even after removing the strategy - - require(isMintWhitelistedStrategy[strategyAddr], "Not whitelisted"); - - isMintWhitelistedStrategy[strategyAddr] = false; - - emit StrategyRemovedFromMintWhitelist(strategistAddr); - } } diff --git a/contracts/deploy/sonic/009_curve_amo.js b/contracts/deploy/sonic/009_curve_amo.js index d1d4ef58fb..49bbff5908 100644 --- a/contracts/deploy/sonic/009_curve_amo.js +++ b/contracts/deploy/sonic/009_curve_amo.js @@ -64,6 +64,12 @@ module.exports = deployOnSonic( ) ); + // Deploy a new Vault Core implementation + const dOSonicVaultCore = await deployWithConfirmation("OSonicVaultCore", [ + addresses.sonic.wS, + ]); + console.log(`Deployed Vault Core to ${dOSonicVaultCore.address}`); + // Deploy a new Vault Admin implementation const dOSonicVaultAdmin = await deployWithConfirmation("OSonicVaultAdmin", [ addresses.sonic.wS, @@ -74,19 +80,25 @@ module.exports = deployOnSonic( return { actions: [ - // 1. Upgrade the VaultAdmin + // 1. Upgrade Vault proxy to new VaultCore + { + contract: cOSonicVaultProxy, + signature: "upgradeTo(address)", + args: [dOSonicVaultCore.address], + }, + // 2. Upgrade the VaultAdmin { contract: cOSonicVaultAdmin, signature: "setAdminImpl(address)", args: [dOSonicVaultAdmin.address], }, - // 2. Approve strategy on vault + // 3. Approve strategy on vault { contract: cOSonicVaultAdmin, signature: "approveStrategy(address)", args: [cSonicCurveAMOStrategyProxy.address], }, - // 3. Add strategy to mint whitelist + // 4. Add strategy to mint whitelist { contract: cOSonicVaultAdmin, signature: "addStrategyToMintWhitelist(address)", diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 37e2723342..2da997e1f3 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -395,8 +395,8 @@ addresses.sonic.SwapXOsGEMSxMultisigBooster = // Curve on Sonic addresses.sonic.CRV = "0x5Af79133999f7908953E94b7A5CF367740Ebee35"; addresses.sonic.WS_OS = {}; -addresses.sonic.WS_OS.pool = "0x7180f41a71f13fac52d2cfb17911f5810c8b0bb9"; -addresses.sonic.WS_OS.gauge = "0x9ca6de419e9fc7bac876de07f0f6ec96331ba207"; +addresses.sonic.WS_OS.pool = "0x7180F41A71f13FaC52d2CfB17911f5810c8B0BB9"; +addresses.sonic.WS_OS.gauge = "0x9CA6dE419e9fc7bAC876DE07F0f6Ec96331Ba207"; addresses.sonic.childLiquidityGaugeFactory = "0xf3A431008396df8A8b2DF492C913706BDB0874ef"; From 67d078a2b1509babe17bf0cec7e137d625398a9c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 13 Feb 2025 12:24:07 +1100 Subject: [PATCH 05/80] FIxed address used in StrategyAddedToMintWhitelist and StrategyRemovedFromMintWhitelist events --- contracts/contracts/vault/OETHVaultAdmin.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/vault/OETHVaultAdmin.sol b/contracts/contracts/vault/OETHVaultAdmin.sol index cf937b47d7..b80c300b36 100644 --- a/contracts/contracts/vault/OETHVaultAdmin.sol +++ b/contracts/contracts/vault/OETHVaultAdmin.sol @@ -42,7 +42,7 @@ contract OETHVaultAdmin is VaultAdmin { isMintWhitelistedStrategy[strategyAddr] = true; - emit StrategyAddedToMintWhitelist(strategistAddr); + emit StrategyAddedToMintWhitelist(strategyAddr); } /** @@ -60,7 +60,7 @@ contract OETHVaultAdmin is VaultAdmin { isMintWhitelistedStrategy[strategyAddr] = false; - emit StrategyRemovedFromMintWhitelist(strategistAddr); + emit StrategyRemovedFromMintWhitelist(strategyAddr); } /// @dev Simplified version of the deposit function as WETH is the only supported asset. From 6a6cf977104d0a777713dce4d0e43c0e46861054 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 13 Feb 2025 12:38:35 +1100 Subject: [PATCH 06/80] Fixed Base unit tests --- contracts/deploy/base/000_mock.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/deploy/base/000_mock.js b/contracts/deploy/base/000_mock.js index af6b617cdc..d06adc7ab5 100644 --- a/contracts/deploy/base/000_mock.js +++ b/contracts/deploy/base/000_mock.js @@ -87,7 +87,9 @@ const deployCore = async () => { const dOETHbVaultCore = await deployWithConfirmation("OETHBaseVaultCore", [ cWETH.address, ]); - const dOETHbVaultAdmin = await deployWithConfirmation("OETHBaseVaultAdmin"); + const dOETHbVaultAdmin = await deployWithConfirmation("OETHBaseVaultAdmin", [ + cWETH.address, + ]); // Get contract instances const cOETHb = await ethers.getContractAt("OETHBase", cOETHbProxy.address); From 163dbffee4a6a55ffeec0e6eb7a1c19f391107b5 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 13 Feb 2025 12:40:33 +1100 Subject: [PATCH 07/80] Generated latest contract diagrams --- contracts/contracts/vault/README.md | 48 +- contracts/docs/OETHBaseHierarchy.svg | 46 -- contracts/docs/OETHBaseVaultAdminSquashed.svg | 167 +++++++ contracts/docs/OETHBaseVaultCoreSquashed.svg | 173 +++++++ contracts/docs/OETHBaseVaultStorage.svg | 382 ++++++++++++++ contracts/docs/OETHHarvesterSimple.svg | 74 +++ contracts/docs/OETHVaultAdminSquashed.svg | 291 +++++------ contracts/docs/OETHVaultCoreSquashed.svg | 299 +++++------ contracts/docs/OETHVaultHierarchy.svg | 157 ------ contracts/docs/OETHVaultStorage.svg | 464 +++++++++--------- contracts/docs/OSonicVaultAdminSquashed.svg | 218 ++++---- contracts/docs/OSonicVaultCoreSquashed.svg | 8 +- contracts/docs/VaultHierarchy.svg | 250 ++++++---- contracts/docs/generate.sh | 14 +- 14 files changed, 1640 insertions(+), 951 deletions(-) delete mode 100644 contracts/docs/OETHBaseHierarchy.svg create mode 100644 contracts/docs/OETHBaseVaultAdminSquashed.svg create mode 100644 contracts/docs/OETHBaseVaultCoreSquashed.svg create mode 100644 contracts/docs/OETHBaseVaultStorage.svg create mode 100644 contracts/docs/OETHHarvesterSimple.svg delete mode 100644 contracts/docs/OETHVaultHierarchy.svg diff --git a/contracts/contracts/vault/README.md b/contracts/contracts/vault/README.md index b754ce591c..651a49f2d6 100644 --- a/contracts/contracts/vault/README.md +++ b/contracts/contracts/vault/README.md @@ -1,37 +1,61 @@ # Diagrams -## Vault - -### Hierarchy +## Hierarchy ![Vault Core Hierarchy](../../docs/VaultHierarchy.svg) -### Storage - -![Vault Storage](../../docs/VaultStorage.svg) +## OUSD Vault ## Vault Core Squashed ![Vault Core Squashed](../../docs/VaultCoreSquashed.svg) +### Storage + +![Vault Storage](../../docs/VaultStorage.svg) + ## Vault Admin Squashed ![Vault Admin Squashed](../../docs/VaultAdminSquashed.svg) ## OETH Vault -### Hierarchy +## Vault Core Squashed + +![OETH Vault Core Squashed](../../docs/OETHVaultCoreSquashed.svg) + +## Vault Admin Squashed -![OETH Vault Core Hierarchy](../../docs/OETHVaultHierarchy.svg) +![OETH Vault Admin Squashed](../../docs/OETHVaultAdminSquashed.svg) ### Storage ![OETH Vault Storage](../../docs/OETHVaultStorage.svg) -## OETH Vault Core Squashed +## Base OETH Vault -![OETH Vault Core Squashed](../../docs/OETHVaultCoreSquashed.svg) +## Vault Core Squashed -## OETH Vault Admin Squashed +![OETH Vault Core Squashed](../../docs/OETHBaseVaultCoreSquashed.svg) -![OETH Vault Admin Squashed](../../docs/OETHVaultAdminSquashed.svg) +## Vault Admin Squashed + +![OETH Vault Admin Squashed](../../docs/OETHBaseVaultAdminSquashed.svg) + +### Storage + +![OETH Vault Storage](../../docs/OETHBaseVaultStorage.svg) + +## Origin Sonic (OS) Vault + +## Vault Core Squashed + +![Sonic Vault Core Squashed](../../docs/OSonicVaultCoreSquashed.svg) + +## Vault Admin Squashed + +![Sonic Vault Admin Squashed](../../docs/OSonicVaultAdminSquashed.svg) + +### Storage + +![Sonic Vault Storage](../../docs/OSonicVaultStorage.svg) \ No newline at end of file diff --git a/contracts/docs/OETHBaseHierarchy.svg b/contracts/docs/OETHBaseHierarchy.svg deleted file mode 100644 index 96c9e666f7..0000000000 --- a/contracts/docs/OETHBaseHierarchy.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - -UmlClassDiagram - - - -21 - -Governable -../contracts/governance/Governable.sol - - - -243 - -OETHBase -../contracts/token/OETHBase.sol - - - -245 - -OUSD -../contracts/token/OUSD.sol - - - -243->245 - - - - - -245->21 - - - - - diff --git a/contracts/docs/OETHBaseVaultAdminSquashed.svg b/contracts/docs/OETHBaseVaultAdminSquashed.svg new file mode 100644 index 0000000000..a4f23c6a14 --- /dev/null +++ b/contracts/docs/OETHBaseVaultAdminSquashed.svg @@ -0,0 +1,167 @@ + + + + + + +UmlClassDiagram + + + +282 + +OETHBaseVaultAdmin +../contracts/vault/OETHBaseVaultAdmin.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _deprecated_rebaseHooksAddr: address <<VaultStorage>> +   _deprecated_uniswapAddr: address <<VaultStorage>> +   _deprecated_swapTokens: address[] <<VaultStorage>> +   __gap: uint256[44] <<VaultStorage>> +Internal: +   assets: mapping(address=>Asset) <<VaultStorage>> +   allAssets: address[] <<VaultStorage>> +   strategies: mapping(address=>Strategy) <<VaultStorage>> +   allStrategies: address[] <<VaultStorage>> +   oUSD: OUSD <<VaultStorage>> +   swapConfig: SwapConfig <<VaultStorage>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   priceProvider: address <<VaultStorage>> +   rebasePaused: bool <<VaultStorage>> +   capitalPaused: bool <<VaultStorage>> +   redeemFeeBps: uint256 <<VaultStorage>> +   vaultBuffer: uint256 <<VaultStorage>> +   autoAllocateThreshold: uint256 <<VaultStorage>> +   rebaseThreshold: uint256 <<VaultStorage>> +   adminImplPosition: bytes32 <<VaultStorage>> +   strategistAddr: address <<VaultStorage>> +   assetDefaultStrategies: mapping(address=>address) <<VaultStorage>> +   maxSupplyDiff: uint256 <<VaultStorage>> +   trusteeAddress: address <<VaultStorage>> +   trusteeFeeBps: uint256 <<VaultStorage>> +   MINT_MINIMUM_UNIT_PRICE: uint256 <<VaultStorage>> +   ousdMetaStrategy: address <<VaultStorage>> +   netOusdMintedForStrategy: int256 <<VaultStorage>> +   netOusdMintForStrategyThreshold: uint256 <<VaultStorage>> +   MIN_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   MAX_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   isMintWhitelistedStrategy: mapping(address=>bool) <<VaultStorage>> +   dripper: address <<VaultStorage>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<VaultStorage>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<VaultStorage>> +   withdrawalClaimDelay: uint256 <<VaultStorage>> +   weth: address <<OETHVaultAdmin>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _swapCollateral(address, address, uint256, uint256, bytes): uint256 <<OETHVaultAdmin>> +    _depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> +    _withdrawFromStrategy(_recipient: address, _strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> +    _withdrawAllFromStrategy(_strategyAddr: address) <<OETHVaultAdmin>> +    _withdrawAllFromStrategies() <<OETHVaultAdmin>> +    _cacheDecimals(token: address) <<VaultAdmin>> +    _wethAvailable(): (wethAvailable: uint256) <<OETHVaultAdmin>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>> +    setPriceProvider(_priceProvider: address) <<onlyGovernor>> <<VaultAdmin>> +    setRedeemFeeBps(_redeemFeeBps: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setVaultBuffer(_vaultBuffer: uint256) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    setAutoAllocateThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setRebaseThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setStrategistAddr(_address: address) <<onlyGovernor>> <<VaultAdmin>> +    setAssetDefaultStrategy(_asset: address, _strategy: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    setNetOusdMintForStrategyThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setDripper(_dripper: address) <<onlyGovernor>> <<VaultAdmin>> +    setWithdrawalClaimDelay(_delay: uint256) <<onlyGovernor>> <<VaultAdmin>> +    swapCollateral(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _minToAssetAmount: uint256, _data: bytes): (toAssetAmount: uint256) <<nonReentrant, onlyGovernorOrStrategist>> <<VaultAdmin>> +    setSwapper(_swapperAddr: address) <<onlyGovernor>> <<VaultAdmin>> +    swapper(): (swapper_: address) <<VaultAdmin>> +    setSwapAllowedUndervalue(_basis: uint16) <<onlyGovernor>> <<VaultAdmin>> +    allowedSwapUndervalue(): (value: uint256) <<VaultAdmin>> +    setOracleSlippage(_asset: address, _allowedOracleSlippageBps: uint16) <<onlyGovernor>> <<VaultAdmin>> +    supportAsset(_asset: address, _unitConversion: uint8) <<onlyGovernor>> <<VaultAdmin>> +    removeAsset(_asset: address) <<onlyGovernor>> <<VaultAdmin>> +    cacheDecimals(_asset: address) <<onlyGovernor>> <<VaultAdmin>> +    approveStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> +    removeStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> +    depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> +    withdrawFromStrategy(_strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> +    setMaxSupplyDiff(_maxSupplyDiff: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setTrusteeAddress(_address: address) <<onlyGovernor>> <<VaultAdmin>> +    setTrusteeFeeBps(_basis: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setOusdMetaStrategy(_ousdMetaStrategy: address) <<onlyGovernor>> <<VaultAdmin>> +    pauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    unpauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    pauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    unpauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<VaultAdmin>> +    withdrawAllFromStrategy(_strategyAddr: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    withdrawAllFromStrategies() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    addStrategyToMintWhitelist(strategyAddr: address) <<onlyGovernor>> <<OETHVaultAdmin>> +    removeStrategyFromMintWhitelist(strategyAddr: address) <<onlyGovernor>> <<OETHVaultAdmin>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> AssetSupported(_asset: address) <<VaultStorage>> +    <<event>> AssetRemoved(_asset: address) <<VaultStorage>> +    <<event>> AssetDefaultStrategyUpdated(_asset: address, _strategy: address) <<VaultStorage>> +    <<event>> AssetAllocated(_asset: address, _strategy: address, _amount: uint256) <<VaultStorage>> +    <<event>> StrategyApproved(_addr: address) <<VaultStorage>> +    <<event>> StrategyRemoved(_addr: address) <<VaultStorage>> +    <<event>> Mint(_addr: address, _value: uint256) <<VaultStorage>> +    <<event>> Redeem(_addr: address, _value: uint256) <<VaultStorage>> +    <<event>> CapitalPaused() <<VaultStorage>> +    <<event>> CapitalUnpaused() <<VaultStorage>> +    <<event>> RebasePaused() <<VaultStorage>> +    <<event>> RebaseUnpaused() <<VaultStorage>> +    <<event>> VaultBufferUpdated(_vaultBuffer: uint256) <<VaultStorage>> +    <<event>> OusdMetaStrategyUpdated(_ousdMetaStrategy: address) <<VaultStorage>> +    <<event>> RedeemFeeUpdated(_redeemFeeBps: uint256) <<VaultStorage>> +    <<event>> PriceProviderUpdated(_priceProvider: address) <<VaultStorage>> +    <<event>> AllocateThresholdUpdated(_threshold: uint256) <<VaultStorage>> +    <<event>> RebaseThresholdUpdated(_threshold: uint256) <<VaultStorage>> +    <<event>> StrategistUpdated(_address: address) <<VaultStorage>> +    <<event>> MaxSupplyDiffChanged(maxSupplyDiff: uint256) <<VaultStorage>> +    <<event>> YieldDistribution(_to: address, _yield: uint256, _fee: uint256) <<VaultStorage>> +    <<event>> TrusteeFeeBpsChanged(_basis: uint256) <<VaultStorage>> +    <<event>> TrusteeAddressChanged(_address: address) <<VaultStorage>> +    <<event>> NetOusdMintForStrategyThresholdChanged(_threshold: uint256) <<VaultStorage>> +    <<event>> SwapperChanged(_address: address) <<VaultStorage>> +    <<event>> SwapAllowedUndervalueChanged(_basis: uint256) <<VaultStorage>> +    <<event>> SwapSlippageChanged(_asset: address, _basis: uint256) <<VaultStorage>> +    <<event>> Swapped(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _toAssetAmount: uint256) <<VaultStorage>> +    <<event>> StrategyAddedToMintWhitelist(strategy: address) <<VaultStorage>> +    <<event>> StrategyRemovedFromMintWhitelist(strategy: address) <<VaultStorage>> +    <<event>> DripperChanged(_dripper: address) <<VaultStorage>> +    <<event>> WithdrawalRequested(_withdrawer: address, _requestId: uint256, _amount: uint256, _queued: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimed(_withdrawer: address, _requestId: uint256, _amount: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimable(_claimable: uint256, _newClaimable: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimDelayUpdated(_newDelay: uint256) <<VaultStorage>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyGovernorOrStrategist() <<VaultAdmin>> +    constructor() <<Governable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    constructor(_weth: address) <<OETHBaseVaultAdmin>> + + + diff --git a/contracts/docs/OETHBaseVaultCoreSquashed.svg b/contracts/docs/OETHBaseVaultCoreSquashed.svg new file mode 100644 index 0000000000..194fb658f8 --- /dev/null +++ b/contracts/docs/OETHBaseVaultCoreSquashed.svg @@ -0,0 +1,173 @@ + + + + + + +UmlClassDiagram + + + +283 + +OETHBaseVaultCore +../contracts/vault/OETHBaseVaultCore.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _deprecated_rebaseHooksAddr: address <<VaultStorage>> +   _deprecated_uniswapAddr: address <<VaultStorage>> +   _deprecated_swapTokens: address[] <<VaultStorage>> +   __gap: uint256[44] <<VaultStorage>> +   __gap: uint256[50] <<OETHVaultCore>> +Internal: +   assets: mapping(address=>Asset) <<VaultStorage>> +   allAssets: address[] <<VaultStorage>> +   strategies: mapping(address=>Strategy) <<VaultStorage>> +   allStrategies: address[] <<VaultStorage>> +   oUSD: OUSD <<VaultStorage>> +   swapConfig: SwapConfig <<VaultStorage>> +   MAX_INT: uint256 <<VaultCore>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   priceProvider: address <<VaultStorage>> +   rebasePaused: bool <<VaultStorage>> +   capitalPaused: bool <<VaultStorage>> +   redeemFeeBps: uint256 <<VaultStorage>> +   vaultBuffer: uint256 <<VaultStorage>> +   autoAllocateThreshold: uint256 <<VaultStorage>> +   rebaseThreshold: uint256 <<VaultStorage>> +   adminImplPosition: bytes32 <<VaultStorage>> +   strategistAddr: address <<VaultStorage>> +   assetDefaultStrategies: mapping(address=>address) <<VaultStorage>> +   maxSupplyDiff: uint256 <<VaultStorage>> +   trusteeAddress: address <<VaultStorage>> +   trusteeFeeBps: uint256 <<VaultStorage>> +   MINT_MINIMUM_UNIT_PRICE: uint256 <<VaultStorage>> +   ousdMetaStrategy: address <<VaultStorage>> +   netOusdMintedForStrategy: int256 <<VaultStorage>> +   netOusdMintForStrategyThreshold: uint256 <<VaultStorage>> +   MIN_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   MAX_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   isMintWhitelistedStrategy: mapping(address=>bool) <<VaultStorage>> +   dripper: address <<VaultStorage>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<VaultStorage>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<VaultStorage>> +   withdrawalClaimDelay: uint256 <<VaultStorage>> +   weth: address <<OETHVaultCore>> +   wethAssetIndex: uint256 <<OETHVaultCore>> + +Private: +    abs(x: int256): uint256 <<VaultCore>> +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _mint(_asset: address, _amount: uint256, _minimumOusdAmount: uint256) <<OETHVaultCore>> +    _redeem(_amount: uint256, _minimumUnitAmount: uint256) <<OETHBaseVaultCore>> +    _postRedeem(_amount: uint256) <<VaultCore>> +    _allocate() <<OETHVaultCore>> +    _rebase(): uint256 <<whenNotRebasePaused>> <<VaultCore>> +    _totalValue(): (value: uint256) <<OETHVaultCore>> +    _totalValueInVault(): (value: uint256) <<OETHVaultCore>> +    _totalValueInStrategies(): (value: uint256) <<VaultCore>> +    _totalValueInStrategy(_strategyAddr: address): (value: uint256) <<VaultCore>> +    _checkBalance(_asset: address): (balance: uint256) <<OETHVaultCore>> +    _calculateRedeemOutputs(_amount: uint256): (outputs: uint256[]) <<OETHVaultCore>> +    _toUnits(_raw: uint256, _asset: address): uint256 <<VaultCore>> +    _toUnitPrice(_asset: address, isMint: bool): (price: uint256) <<VaultCore>> +    _getDecimals(_asset: address): (decimals: uint256) <<VaultCore>> +    _claimWithdrawal(requestId: uint256): (amount: uint256) <<OETHVaultCore>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<OETHVaultCore>> +    _wethAvailable(): (wethAvailable: uint256) <<OETHVaultCore>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>> +    initialize(_priceProvider: address, _oToken: address) <<onlyGovernor, initializer>> <<VaultInitializer>> +    mint(_asset: address, _amount: uint256, _minimumOusdAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> +    mintForStrategy(amount: uint256) <<whenNotCapitalPaused>> <<OETHVaultCore>> +    redeem(_amount: uint256, _minimumUnitAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> +    burnForStrategy(amount: uint256) <<whenNotCapitalPaused>> <<OETHVaultCore>> +    redeemAll(_minimumUnitAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> +    allocate() <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> +    rebase() <<nonReentrant>> <<VaultCore>> +    totalValue(): (value: uint256) <<VaultCore>> +    checkBalance(_asset: address): uint256 <<VaultCore>> +    calculateRedeemOutputs(_amount: uint256): uint256[] <<VaultCore>> +    priceUnitMint(asset: address): (price: uint256) <<VaultCore>> +    priceUnitRedeem(asset: address): (price: uint256) <<VaultCore>> +    getAllAssets(): address[] <<VaultCore>> +    getStrategyCount(): uint256 <<VaultCore>> +    getAllStrategies(): address[] <<VaultCore>> +    isSupportedAsset(_asset: address): bool <<VaultCore>> +    null() <<VaultCore>> +    cacheWETHAssetIndex() <<onlyGovernor>> <<OETHVaultCore>> +    requestWithdrawal(_amount: uint256): (requestId: uint256, queued: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> +    claimWithdrawal(_requestId: uint256): (amount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> +    claimWithdrawals(_requestIds: uint256[]): (amounts: uint256[], totalAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> +    addWithdrawalQueueLiquidity() <<OETHVaultCore>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> AssetSupported(_asset: address) <<VaultStorage>> +    <<event>> AssetRemoved(_asset: address) <<VaultStorage>> +    <<event>> AssetDefaultStrategyUpdated(_asset: address, _strategy: address) <<VaultStorage>> +    <<event>> AssetAllocated(_asset: address, _strategy: address, _amount: uint256) <<VaultStorage>> +    <<event>> StrategyApproved(_addr: address) <<VaultStorage>> +    <<event>> StrategyRemoved(_addr: address) <<VaultStorage>> +    <<event>> Mint(_addr: address, _value: uint256) <<VaultStorage>> +    <<event>> Redeem(_addr: address, _value: uint256) <<VaultStorage>> +    <<event>> CapitalPaused() <<VaultStorage>> +    <<event>> CapitalUnpaused() <<VaultStorage>> +    <<event>> RebasePaused() <<VaultStorage>> +    <<event>> RebaseUnpaused() <<VaultStorage>> +    <<event>> VaultBufferUpdated(_vaultBuffer: uint256) <<VaultStorage>> +    <<event>> OusdMetaStrategyUpdated(_ousdMetaStrategy: address) <<VaultStorage>> +    <<event>> RedeemFeeUpdated(_redeemFeeBps: uint256) <<VaultStorage>> +    <<event>> PriceProviderUpdated(_priceProvider: address) <<VaultStorage>> +    <<event>> AllocateThresholdUpdated(_threshold: uint256) <<VaultStorage>> +    <<event>> RebaseThresholdUpdated(_threshold: uint256) <<VaultStorage>> +    <<event>> StrategistUpdated(_address: address) <<VaultStorage>> +    <<event>> MaxSupplyDiffChanged(maxSupplyDiff: uint256) <<VaultStorage>> +    <<event>> YieldDistribution(_to: address, _yield: uint256, _fee: uint256) <<VaultStorage>> +    <<event>> TrusteeFeeBpsChanged(_basis: uint256) <<VaultStorage>> +    <<event>> TrusteeAddressChanged(_address: address) <<VaultStorage>> +    <<event>> NetOusdMintForStrategyThresholdChanged(_threshold: uint256) <<VaultStorage>> +    <<event>> SwapperChanged(_address: address) <<VaultStorage>> +    <<event>> SwapAllowedUndervalueChanged(_basis: uint256) <<VaultStorage>> +    <<event>> SwapSlippageChanged(_asset: address, _basis: uint256) <<VaultStorage>> +    <<event>> Swapped(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _toAssetAmount: uint256) <<VaultStorage>> +    <<event>> StrategyAddedToMintWhitelist(strategy: address) <<VaultStorage>> +    <<event>> StrategyRemovedFromMintWhitelist(strategy: address) <<VaultStorage>> +    <<event>> DripperChanged(_dripper: address) <<VaultStorage>> +    <<event>> WithdrawalRequested(_withdrawer: address, _requestId: uint256, _amount: uint256, _queued: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimed(_withdrawer: address, _requestId: uint256, _amount: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimable(_claimable: uint256, _newClaimable: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimDelayUpdated(_newDelay: uint256) <<VaultStorage>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> whenNotRebasePaused() <<VaultCore>> +    <<modifier>> whenNotCapitalPaused() <<VaultCore>> +    <<modifier>> onlyOusdMetaStrategy() <<VaultCore>> +    constructor() <<Governable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    getAssetCount(): uint256 <<VaultCore>> +    getAssetConfig(_asset: address): (config: Asset) <<VaultCore>> +    constructor(_weth: address) <<OETHBaseVaultCore>> + + + diff --git a/contracts/docs/OETHBaseVaultStorage.svg b/contracts/docs/OETHBaseVaultStorage.svg new file mode 100644 index 0000000000..f768dd006b --- /dev/null +++ b/contracts/docs/OETHBaseVaultStorage.svg @@ -0,0 +1,382 @@ + + + + + + +StorageDiagram + + + +9 + +OETHBaseVaultCore <<Contract>> + +slot + +0 + +1-50 + +51 + +52 + +53 + +54 + +55 + +56 + +57 + +58 + +59 + +60 + +61 + +62 + +63 + +64 + +65 + +66 + +67 + +68 + +69 + +70 + +71 + +72 + +73 + +74 + +75-76 + +77 + +78 + +79-122 + +123 + +124-173 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.______gap (1600) + +mapping(address=>Asset): VaultStorage.assets (32) + +address[]: VaultStorage.allAssets (32) + +mapping(address=>Strategy): VaultStorage.strategies (32) + +address[]: VaultStorage.allStrategies (32) + +unallocated (10) + +bool: VaultStorage.capitalPaused (1) + +bool: VaultStorage.rebasePaused (1) + +address: VaultStorage.priceProvider (20) + +uint256: VaultStorage.redeemFeeBps (32) + +uint256: VaultStorage.vaultBuffer (32) + +uint256: VaultStorage.autoAllocateThreshold (32) + +uint256: VaultStorage.rebaseThreshold (32) + +unallocated (12) + +OUSD: VaultStorage.oUSD (20) + +unallocated (12) + +address: VaultStorage._deprecated_rebaseHooksAddr (20) + +unallocated (12) + +address: VaultStorage._deprecated_uniswapAddr (20) + +unallocated (12) + +address: VaultStorage.strategistAddr (20) + +mapping(address=>address): VaultStorage.assetDefaultStrategies (32) + +uint256: VaultStorage.maxSupplyDiff (32) + +unallocated (12) + +address: VaultStorage.trusteeAddress (20) + +uint256: VaultStorage.trusteeFeeBps (32) + +address[]: VaultStorage._deprecated_swapTokens (32) + +unallocated (12) + +address: VaultStorage.ousdMetaStrategy (20) + +int256: VaultStorage.netOusdMintedForStrategy (32) + +uint256: VaultStorage.netOusdMintForStrategyThreshold (32) + +SwapConfig: VaultStorage.swapConfig (32) + +mapping(address=>bool): VaultStorage.isMintWhitelistedStrategy (32) + +unallocated (12) + +address: VaultStorage.dripper (20) + +WithdrawalQueueMetadata: VaultStorage.withdrawalQueueMetadata (64) + +mapping(uint256=>WithdrawalRequest): VaultStorage.withdrawalRequests (32) + +uint256: VaultStorage.withdrawalClaimDelay (32) + +uint256[44]: VaultStorage.__gap (1408) + +uint256: OETHVaultCore.wethAssetIndex (32) + +uint256[50]: OETHVaultCore.__gap (1600) + + + +1 + +Asset <<Struct>> + +offset + +0 + +type: variable (bytes) + +unallocated (27) + +uint16: allowedOracleSlippageBps (2) + +uint8: decimals (1) + +UnitConversion: unitConversion (1) + +bool: isSupported (1) + + + +9:8->1 + + + + + +2 + +address[]: allAssets <<Array>> +0x46bddb1178e94d7f2892ff5f366840eb658911794f2c3a44c450aa2c505186c1 + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +9:10->2 + + + + + +3 + +Strategy <<Struct>> + +offset + +0 + +1 + +type: variable (bytes) + +unallocated (31) + +bool: isSupported (1) + +uint256: _deprecated (32) + + + +9:13->3 + + + + + +4 + +address[]: allStrategies <<Array>> +0x4a11f94e20a93c79f6ec743a1954ec4fc2c08429ae2122118bf234b2185c81b8 + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +9:15->4 + + + + + +5 + +address[]: _deprecated_swapTokens <<Array>> +0x9b22d3d61959b4d3528b1d8ba932c96fbe302b36a1aad1d95cab54f9e0a135ea + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +9:32->5 + + + + + +6 + +SwapConfig <<Struct>> + +slot + +72 + +type: variable (bytes) + +unallocated (10) + +uint16: allowedUndervalueBps (2) + +address: swapper (20) + + + +9:38->6 + + + + + +7 + +WithdrawalQueueMetadata <<Struct>> + +slot + +75 + +76 + +type: variable (bytes) + +uint128: claimable (16) + +uint128: queued (16) + +uint128: nextWithdrawalIndex (16) + +uint128: claimed (16) + + + +9:45->7 + + + + + +8 + +WithdrawalRequest <<Struct>> + +offset + +0 + +1 + +type: variable (bytes) + +unallocated (6) + +uint40: timestamp (5) + +bool: claimed (1) + +address: withdrawer (20) + +uint128: queued (16) + +uint128: amount (16) + + + +9:51->8 + + + + + diff --git a/contracts/docs/OETHHarvesterSimple.svg b/contracts/docs/OETHHarvesterSimple.svg new file mode 100644 index 0000000000..df4be2d1ff --- /dev/null +++ b/contracts/docs/OETHHarvesterSimple.svg @@ -0,0 +1,74 @@ + + + + + + +StorageDiagram + + + +2 + +OETHHarvesterSimple <<Contract>> +0x399B69Bf06CCec7A53Befea14771059d39a3617a + +slot + +0 + +1-50 + +51 + +type: <inherited contract>.variable (bytes) + +unallocated (12) + +address: Strategizable.strategistAddr (20) + +uint256[50]: Strategizable.__gap (1600) + +mapping(address=>bool): supportedStrategies (32) + + + +1 + +uint256[50]: __gap <<Array>> + +slot + +1 + +2 + +3-48 + +49 + +50 + +type: variable (bytes) + +uint256 (32) + +uint256 (32) + +---- (1472) + +uint256 (32) + +uint256 (32) + + + +2:7->1 + + + + + diff --git a/contracts/docs/OETHVaultAdminSquashed.svg b/contracts/docs/OETHVaultAdminSquashed.svg index d5af64e4d8..42ba2da7ee 100644 --- a/contracts/docs/OETHVaultAdminSquashed.svg +++ b/contracts/docs/OETHVaultAdminSquashed.svg @@ -4,151 +4,156 @@ - - + + UmlClassDiagram - - + + -233 - -OETHVaultAdmin -../contracts/vault/OETHVaultAdmin.sol - -Private: -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   ______gap: uint256[50] <<Initializable>> -   governorPosition: bytes32 <<Governable>> -   pendingGovernorPosition: bytes32 <<Governable>> -   reentryStatusPosition: bytes32 <<Governable>> -   _deprecated_rebaseHooksAddr: address <<VaultStorage>> -   _deprecated_uniswapAddr: address <<VaultStorage>> -   _deprecated_swapTokens: address[] <<VaultStorage>> -   __gap: uint256[45] <<VaultStorage>> -Internal: -   assets: mapping(address=>Asset) <<VaultStorage>> -   allAssets: address[] <<VaultStorage>> -   strategies: mapping(address=>Strategy) <<VaultStorage>> -   allStrategies: address[] <<VaultStorage>> -   oUSD: OUSD <<VaultStorage>> -   swapConfig: SwapConfig <<VaultStorage>> -Public: -   _NOT_ENTERED: uint256 <<Governable>> -   _ENTERED: uint256 <<Governable>> -   priceProvider: address <<VaultStorage>> -   rebasePaused: bool <<VaultStorage>> -   capitalPaused: bool <<VaultStorage>> -   redeemFeeBps: uint256 <<VaultStorage>> -   vaultBuffer: uint256 <<VaultStorage>> -   autoAllocateThreshold: uint256 <<VaultStorage>> -   rebaseThreshold: uint256 <<VaultStorage>> -   adminImplPosition: bytes32 <<VaultStorage>> -   strategistAddr: address <<VaultStorage>> -   assetDefaultStrategies: mapping(address=>address) <<VaultStorage>> -   maxSupplyDiff: uint256 <<VaultStorage>> -   trusteeAddress: address <<VaultStorage>> -   trusteeFeeBps: uint256 <<VaultStorage>> -   MINT_MINIMUM_UNIT_PRICE: uint256 <<VaultStorage>> -   ousdMetaStrategy: address <<VaultStorage>> -   netOusdMintedForStrategy: int256 <<VaultStorage>> -   netOusdMintForStrategyThreshold: uint256 <<VaultStorage>> -   MIN_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> -   MAX_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> -   isMintWhitelistedStrategy: mapping(address=>bool) <<VaultStorage>> -   dripper: address <<VaultStorage>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<VaultStorage>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<VaultStorage>> -   weth: address <<OETHVaultAdmin>> - -Internal: -    _governor(): (governorOut: address) <<Governable>> -    _pendingGovernor(): (pendingGovernor: address) <<Governable>> -    _setGovernor(newGovernor: address) <<Governable>> -    _setPendingGovernor(newGovernor: address) <<Governable>> -    _changeGovernor(_newGovernor: address) <<Governable>> -    _swapCollateral(address, address, uint256, uint256, bytes): uint256 <<OETHVaultAdmin>> -    _depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> -    _withdrawFromStrategy(_recipient: address, _strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> -    _withdrawAllFromStrategy(_strategyAddr: address) <<OETHVaultAdmin>> -    _withdrawAllFromStrategies() <<OETHVaultAdmin>> -    _cacheDecimals(token: address) <<VaultAdmin>> -    _wethAvailable(): (wethAvailable: uint256) <<OETHVaultAdmin>> -External: -    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> -    claimGovernance() <<Governable>> -    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>> -    setPriceProvider(_priceProvider: address) <<onlyGovernor>> <<VaultAdmin>> -    setRedeemFeeBps(_redeemFeeBps: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setVaultBuffer(_vaultBuffer: uint256) <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    setAutoAllocateThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setRebaseThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setStrategistAddr(_address: address) <<onlyGovernor>> <<VaultAdmin>> -    setAssetDefaultStrategy(_asset: address, _strategy: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    setNetOusdMintForStrategyThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setDripper(_dripper: address) <<onlyGovernor>> <<VaultAdmin>> -    swapCollateral(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _minToAssetAmount: uint256, _data: bytes): (toAssetAmount: uint256) <<nonReentrant, onlyGovernorOrStrategist>> <<VaultAdmin>> -    setSwapper(_swapperAddr: address) <<onlyGovernor>> <<VaultAdmin>> -    swapper(): (swapper_: address) <<VaultAdmin>> -    setSwapAllowedUndervalue(_basis: uint16) <<onlyGovernor>> <<VaultAdmin>> -    allowedSwapUndervalue(): (value: uint256) <<VaultAdmin>> -    setOracleSlippage(_asset: address, _allowedOracleSlippageBps: uint16) <<onlyGovernor>> <<VaultAdmin>> -    supportAsset(_asset: address, _unitConversion: uint8) <<onlyGovernor>> <<VaultAdmin>> -    removeAsset(_asset: address) <<onlyGovernor>> <<VaultAdmin>> -    cacheDecimals(_asset: address) <<onlyGovernor>> <<VaultAdmin>> -    approveStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> -    removeStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> -    depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> -    withdrawFromStrategy(_strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> -    setMaxSupplyDiff(_maxSupplyDiff: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setTrusteeAddress(_address: address) <<onlyGovernor>> <<VaultAdmin>> -    setTrusteeFeeBps(_basis: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setOusdMetaStrategy(_ousdMetaStrategy: address) <<onlyGovernor>> <<VaultAdmin>> -    pauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    unpauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    pauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    unpauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<VaultAdmin>> -    withdrawAllFromStrategy(_strategyAddr: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    withdrawAllFromStrategies() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -Public: -    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> AssetSupported(_asset: address) <<VaultStorage>> -    <<event>> AssetRemoved(_asset: address) <<VaultStorage>> -    <<event>> AssetDefaultStrategyUpdated(_asset: address, _strategy: address) <<VaultStorage>> -    <<event>> AssetAllocated(_asset: address, _strategy: address, _amount: uint256) <<VaultStorage>> -    <<event>> StrategyApproved(_addr: address) <<VaultStorage>> -    <<event>> StrategyRemoved(_addr: address) <<VaultStorage>> -    <<event>> Mint(_addr: address, _value: uint256) <<VaultStorage>> -    <<event>> Redeem(_addr: address, _value: uint256) <<VaultStorage>> -    <<event>> CapitalPaused() <<VaultStorage>> -    <<event>> CapitalUnpaused() <<VaultStorage>> -    <<event>> RebasePaused() <<VaultStorage>> -    <<event>> RebaseUnpaused() <<VaultStorage>> -    <<event>> VaultBufferUpdated(_vaultBuffer: uint256) <<VaultStorage>> -    <<event>> OusdMetaStrategyUpdated(_ousdMetaStrategy: address) <<VaultStorage>> -    <<event>> RedeemFeeUpdated(_redeemFeeBps: uint256) <<VaultStorage>> -    <<event>> PriceProviderUpdated(_priceProvider: address) <<VaultStorage>> -    <<event>> AllocateThresholdUpdated(_threshold: uint256) <<VaultStorage>> -    <<event>> RebaseThresholdUpdated(_threshold: uint256) <<VaultStorage>> -    <<event>> StrategistUpdated(_address: address) <<VaultStorage>> -    <<event>> MaxSupplyDiffChanged(maxSupplyDiff: uint256) <<VaultStorage>> -    <<event>> YieldDistribution(_to: address, _yield: uint256, _fee: uint256) <<VaultStorage>> -    <<event>> TrusteeFeeBpsChanged(_basis: uint256) <<VaultStorage>> -    <<event>> TrusteeAddressChanged(_address: address) <<VaultStorage>> -    <<event>> NetOusdMintForStrategyThresholdChanged(_threshold: uint256) <<VaultStorage>> -    <<event>> SwapperChanged(_address: address) <<VaultStorage>> -    <<event>> SwapAllowedUndervalueChanged(_basis: uint256) <<VaultStorage>> -    <<event>> SwapSlippageChanged(_asset: address, _basis: uint256) <<VaultStorage>> -    <<event>> Swapped(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _toAssetAmount: uint256) <<VaultStorage>> -    <<event>> DripperChanged(_dripper: address) <<VaultStorage>> -    <<event>> StrategyAddedToMintWhitelist(strategy: address) <<VaultStorage>> -    <<event>> StrategyRemovedFromMintWhitelist(strategy: address) <<VaultStorage>> -    <<event>> WithdrawalRequested(_withdrawer: address, _requestId: uint256, _amount: uint256, _queued: uint256) <<VaultStorage>> -    <<event>> WithdrawalClaimed(_withdrawer: address, _requestId: uint256, _amount: uint256) <<VaultStorage>> -    <<event>> WithdrawalClaimable(_claimable: uint256, _newClaimable: uint256) <<VaultStorage>> +286 + +OETHVaultAdmin +../contracts/vault/OETHVaultAdmin.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _deprecated_rebaseHooksAddr: address <<VaultStorage>> +   _deprecated_uniswapAddr: address <<VaultStorage>> +   _deprecated_swapTokens: address[] <<VaultStorage>> +   __gap: uint256[44] <<VaultStorage>> +Internal: +   assets: mapping(address=>Asset) <<VaultStorage>> +   allAssets: address[] <<VaultStorage>> +   strategies: mapping(address=>Strategy) <<VaultStorage>> +   allStrategies: address[] <<VaultStorage>> +   oUSD: OUSD <<VaultStorage>> +   swapConfig: SwapConfig <<VaultStorage>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   priceProvider: address <<VaultStorage>> +   rebasePaused: bool <<VaultStorage>> +   capitalPaused: bool <<VaultStorage>> +   redeemFeeBps: uint256 <<VaultStorage>> +   vaultBuffer: uint256 <<VaultStorage>> +   autoAllocateThreshold: uint256 <<VaultStorage>> +   rebaseThreshold: uint256 <<VaultStorage>> +   adminImplPosition: bytes32 <<VaultStorage>> +   strategistAddr: address <<VaultStorage>> +   assetDefaultStrategies: mapping(address=>address) <<VaultStorage>> +   maxSupplyDiff: uint256 <<VaultStorage>> +   trusteeAddress: address <<VaultStorage>> +   trusteeFeeBps: uint256 <<VaultStorage>> +   MINT_MINIMUM_UNIT_PRICE: uint256 <<VaultStorage>> +   ousdMetaStrategy: address <<VaultStorage>> +   netOusdMintedForStrategy: int256 <<VaultStorage>> +   netOusdMintForStrategyThreshold: uint256 <<VaultStorage>> +   MIN_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   MAX_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   isMintWhitelistedStrategy: mapping(address=>bool) <<VaultStorage>> +   dripper: address <<VaultStorage>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<VaultStorage>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<VaultStorage>> +   withdrawalClaimDelay: uint256 <<VaultStorage>> +   weth: address <<OETHVaultAdmin>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _swapCollateral(address, address, uint256, uint256, bytes): uint256 <<OETHVaultAdmin>> +    _depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> +    _withdrawFromStrategy(_recipient: address, _strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> +    _withdrawAllFromStrategy(_strategyAddr: address) <<OETHVaultAdmin>> +    _withdrawAllFromStrategies() <<OETHVaultAdmin>> +    _cacheDecimals(token: address) <<VaultAdmin>> +    _wethAvailable(): (wethAvailable: uint256) <<OETHVaultAdmin>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>> +    setPriceProvider(_priceProvider: address) <<onlyGovernor>> <<VaultAdmin>> +    setRedeemFeeBps(_redeemFeeBps: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setVaultBuffer(_vaultBuffer: uint256) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    setAutoAllocateThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setRebaseThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setStrategistAddr(_address: address) <<onlyGovernor>> <<VaultAdmin>> +    setAssetDefaultStrategy(_asset: address, _strategy: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    setNetOusdMintForStrategyThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setDripper(_dripper: address) <<onlyGovernor>> <<VaultAdmin>> +    setWithdrawalClaimDelay(_delay: uint256) <<onlyGovernor>> <<VaultAdmin>> +    swapCollateral(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _minToAssetAmount: uint256, _data: bytes): (toAssetAmount: uint256) <<nonReentrant, onlyGovernorOrStrategist>> <<VaultAdmin>> +    setSwapper(_swapperAddr: address) <<onlyGovernor>> <<VaultAdmin>> +    swapper(): (swapper_: address) <<VaultAdmin>> +    setSwapAllowedUndervalue(_basis: uint16) <<onlyGovernor>> <<VaultAdmin>> +    allowedSwapUndervalue(): (value: uint256) <<VaultAdmin>> +    setOracleSlippage(_asset: address, _allowedOracleSlippageBps: uint16) <<onlyGovernor>> <<VaultAdmin>> +    supportAsset(_asset: address, _unitConversion: uint8) <<onlyGovernor>> <<VaultAdmin>> +    removeAsset(_asset: address) <<onlyGovernor>> <<VaultAdmin>> +    cacheDecimals(_asset: address) <<onlyGovernor>> <<VaultAdmin>> +    approveStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> +    removeStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> +    depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> +    withdrawFromStrategy(_strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> +    setMaxSupplyDiff(_maxSupplyDiff: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setTrusteeAddress(_address: address) <<onlyGovernor>> <<VaultAdmin>> +    setTrusteeFeeBps(_basis: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setOusdMetaStrategy(_ousdMetaStrategy: address) <<onlyGovernor>> <<VaultAdmin>> +    pauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    unpauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    pauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    unpauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<VaultAdmin>> +    withdrawAllFromStrategy(_strategyAddr: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    withdrawAllFromStrategies() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    addStrategyToMintWhitelist(strategyAddr: address) <<onlyGovernor>> <<OETHVaultAdmin>> +    removeStrategyFromMintWhitelist(strategyAddr: address) <<onlyGovernor>> <<OETHVaultAdmin>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> AssetSupported(_asset: address) <<VaultStorage>> +    <<event>> AssetRemoved(_asset: address) <<VaultStorage>> +    <<event>> AssetDefaultStrategyUpdated(_asset: address, _strategy: address) <<VaultStorage>> +    <<event>> AssetAllocated(_asset: address, _strategy: address, _amount: uint256) <<VaultStorage>> +    <<event>> StrategyApproved(_addr: address) <<VaultStorage>> +    <<event>> StrategyRemoved(_addr: address) <<VaultStorage>> +    <<event>> Mint(_addr: address, _value: uint256) <<VaultStorage>> +    <<event>> Redeem(_addr: address, _value: uint256) <<VaultStorage>> +    <<event>> CapitalPaused() <<VaultStorage>> +    <<event>> CapitalUnpaused() <<VaultStorage>> +    <<event>> RebasePaused() <<VaultStorage>> +    <<event>> RebaseUnpaused() <<VaultStorage>> +    <<event>> VaultBufferUpdated(_vaultBuffer: uint256) <<VaultStorage>> +    <<event>> OusdMetaStrategyUpdated(_ousdMetaStrategy: address) <<VaultStorage>> +    <<event>> RedeemFeeUpdated(_redeemFeeBps: uint256) <<VaultStorage>> +    <<event>> PriceProviderUpdated(_priceProvider: address) <<VaultStorage>> +    <<event>> AllocateThresholdUpdated(_threshold: uint256) <<VaultStorage>> +    <<event>> RebaseThresholdUpdated(_threshold: uint256) <<VaultStorage>> +    <<event>> StrategistUpdated(_address: address) <<VaultStorage>> +    <<event>> MaxSupplyDiffChanged(maxSupplyDiff: uint256) <<VaultStorage>> +    <<event>> YieldDistribution(_to: address, _yield: uint256, _fee: uint256) <<VaultStorage>> +    <<event>> TrusteeFeeBpsChanged(_basis: uint256) <<VaultStorage>> +    <<event>> TrusteeAddressChanged(_address: address) <<VaultStorage>> +    <<event>> NetOusdMintForStrategyThresholdChanged(_threshold: uint256) <<VaultStorage>> +    <<event>> SwapperChanged(_address: address) <<VaultStorage>> +    <<event>> SwapAllowedUndervalueChanged(_basis: uint256) <<VaultStorage>> +    <<event>> SwapSlippageChanged(_asset: address, _basis: uint256) <<VaultStorage>> +    <<event>> Swapped(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _toAssetAmount: uint256) <<VaultStorage>> +    <<event>> StrategyAddedToMintWhitelist(strategy: address) <<VaultStorage>> +    <<event>> StrategyRemovedFromMintWhitelist(strategy: address) <<VaultStorage>> +    <<event>> DripperChanged(_dripper: address) <<VaultStorage>> +    <<event>> WithdrawalRequested(_withdrawer: address, _requestId: uint256, _amount: uint256, _queued: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimed(_withdrawer: address, _requestId: uint256, _amount: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimable(_claimable: uint256, _newClaimable: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimDelayUpdated(_newDelay: uint256) <<VaultStorage>>    <<modifier>> initializer() <<Initializable>>    <<modifier>> onlyGovernor() <<Governable>>    <<modifier>> nonReentrant() <<Governable>> diff --git a/contracts/docs/OETHVaultCoreSquashed.svg b/contracts/docs/OETHVaultCoreSquashed.svg index 7ff1c7040e..4bb8488671 100644 --- a/contracts/docs/OETHVaultCoreSquashed.svg +++ b/contracts/docs/OETHVaultCoreSquashed.svg @@ -4,157 +4,158 @@ - - + + UmlClassDiagram - - + + -234 - -OETHVaultCore -../contracts/vault/OETHVaultCore.sol - -Private: -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   ______gap: uint256[50] <<Initializable>> -   governorPosition: bytes32 <<Governable>> -   pendingGovernorPosition: bytes32 <<Governable>> -   reentryStatusPosition: bytes32 <<Governable>> -   _deprecated_rebaseHooksAddr: address <<VaultStorage>> -   _deprecated_uniswapAddr: address <<VaultStorage>> -   _deprecated_swapTokens: address[] <<VaultStorage>> -   __gap: uint256[45] <<VaultStorage>> -   __gap: uint256[50] <<OETHVaultCore>> -Internal: -   assets: mapping(address=>Asset) <<VaultStorage>> -   allAssets: address[] <<VaultStorage>> -   strategies: mapping(address=>Strategy) <<VaultStorage>> -   allStrategies: address[] <<VaultStorage>> -   oUSD: OUSD <<VaultStorage>> -   swapConfig: SwapConfig <<VaultStorage>> -   MAX_INT: uint256 <<VaultCore>> -Public: -   _NOT_ENTERED: uint256 <<Governable>> -   _ENTERED: uint256 <<Governable>> -   priceProvider: address <<VaultStorage>> -   rebasePaused: bool <<VaultStorage>> -   capitalPaused: bool <<VaultStorage>> -   redeemFeeBps: uint256 <<VaultStorage>> -   vaultBuffer: uint256 <<VaultStorage>> -   autoAllocateThreshold: uint256 <<VaultStorage>> -   rebaseThreshold: uint256 <<VaultStorage>> -   adminImplPosition: bytes32 <<VaultStorage>> -   strategistAddr: address <<VaultStorage>> -   assetDefaultStrategies: mapping(address=>address) <<VaultStorage>> -   maxSupplyDiff: uint256 <<VaultStorage>> -   trusteeAddress: address <<VaultStorage>> -   trusteeFeeBps: uint256 <<VaultStorage>> -   MINT_MINIMUM_UNIT_PRICE: uint256 <<VaultStorage>> -   ousdMetaStrategy: address <<VaultStorage>> -   netOusdMintedForStrategy: int256 <<VaultStorage>> -   netOusdMintForStrategyThreshold: uint256 <<VaultStorage>> -   MIN_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> -   MAX_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> -   isMintWhitelistedStrategy: mapping(address=>bool) <<VaultStorage>> -   dripper: address <<VaultStorage>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<VaultStorage>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<VaultStorage>> -   CLAIM_DELAY: uint256 <<OETHVaultCore>> -   weth: address <<OETHVaultCore>> -   wethAssetIndex: uint256 <<OETHVaultCore>> - -Private: -    abs(x: int256): uint256 <<VaultCore>> -Internal: -    _governor(): (governorOut: address) <<Governable>> -    _pendingGovernor(): (pendingGovernor: address) <<Governable>> -    _setGovernor(newGovernor: address) <<Governable>> -    _setPendingGovernor(newGovernor: address) <<Governable>> -    _changeGovernor(_newGovernor: address) <<Governable>> -    _mint(_asset: address, _amount: uint256, _minimumOusdAmount: uint256) <<OETHVaultCore>> -    _redeem(_amount: uint256, _minimumUnitAmount: uint256) <<OETHVaultCore>> -    _postRedeem(_amount: uint256) <<VaultCore>> -    _allocate() <<OETHVaultCore>> -    _rebase(): uint256 <<whenNotRebasePaused>> <<VaultCore>> -    _totalValue(): (value: uint256) <<OETHVaultCore>> -    _totalValueInVault(): (value: uint256) <<OETHVaultCore>> -    _totalValueInStrategies(): (value: uint256) <<VaultCore>> -    _totalValueInStrategy(_strategyAddr: address): (value: uint256) <<VaultCore>> -    _checkBalance(_asset: address): (balance: uint256) <<OETHVaultCore>> -    _calculateRedeemOutputs(_amount: uint256): (outputs: uint256[]) <<OETHVaultCore>> -    _toUnits(_raw: uint256, _asset: address): uint256 <<VaultCore>> -    _toUnitPrice(_asset: address, isMint: bool): (price: uint256) <<VaultCore>> -    _getDecimals(_asset: address): (decimals: uint256) <<VaultCore>> -    _claimWithdrawal(requestId: uint256): (amount: uint256) <<OETHVaultCore>> -    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<OETHVaultCore>> -    _wethAvailable(): (wethAvailable: uint256) <<OETHVaultCore>> -External: -    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> -    claimGovernance() <<Governable>> -    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>> -    initialize(_priceProvider: address, _oToken: address) <<onlyGovernor, initializer>> <<VaultInitializer>> -    mint(_asset: address, _amount: uint256, _minimumOusdAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> -    mintForStrategy(_amount: uint256) <<whenNotCapitalPaused, onlyOusdMetaStrategy>> <<VaultCore>> -    redeem(_amount: uint256, _minimumUnitAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> -    burnForStrategy(_amount: uint256) <<whenNotCapitalPaused, onlyOusdMetaStrategy>> <<VaultCore>> -    redeemAll(_minimumUnitAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> -    allocate() <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> -    rebase() <<nonReentrant>> <<VaultCore>> -    totalValue(): (value: uint256) <<VaultCore>> -    checkBalance(_asset: address): uint256 <<VaultCore>> -    calculateRedeemOutputs(_amount: uint256): uint256[] <<VaultCore>> -    priceUnitMint(asset: address): (price: uint256) <<VaultCore>> -    priceUnitRedeem(asset: address): (price: uint256) <<VaultCore>> -    getAllAssets(): address[] <<VaultCore>> -    getStrategyCount(): uint256 <<VaultCore>> -    getAllStrategies(): address[] <<VaultCore>> -    isSupportedAsset(_asset: address): bool <<VaultCore>> -    null() <<VaultCore>> -    cacheWETHAssetIndex() <<onlyGovernor>> <<OETHVaultCore>> -    requestWithdrawal(_amount: uint256): (requestId: uint256, queued: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> -    claimWithdrawal(_requestId: uint256): (amount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> -    claimWithdrawals(_requestIds: uint256[]): (amounts: uint256[], totalAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> -    addWithdrawalQueueLiquidity() <<OETHVaultCore>> -Public: -    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> AssetSupported(_asset: address) <<VaultStorage>> -    <<event>> AssetRemoved(_asset: address) <<VaultStorage>> -    <<event>> AssetDefaultStrategyUpdated(_asset: address, _strategy: address) <<VaultStorage>> -    <<event>> AssetAllocated(_asset: address, _strategy: address, _amount: uint256) <<VaultStorage>> -    <<event>> StrategyApproved(_addr: address) <<VaultStorage>> -    <<event>> StrategyRemoved(_addr: address) <<VaultStorage>> -    <<event>> Mint(_addr: address, _value: uint256) <<VaultStorage>> -    <<event>> Redeem(_addr: address, _value: uint256) <<VaultStorage>> -    <<event>> CapitalPaused() <<VaultStorage>> -    <<event>> CapitalUnpaused() <<VaultStorage>> -    <<event>> RebasePaused() <<VaultStorage>> -    <<event>> RebaseUnpaused() <<VaultStorage>> -    <<event>> VaultBufferUpdated(_vaultBuffer: uint256) <<VaultStorage>> -    <<event>> OusdMetaStrategyUpdated(_ousdMetaStrategy: address) <<VaultStorage>> -    <<event>> RedeemFeeUpdated(_redeemFeeBps: uint256) <<VaultStorage>> -    <<event>> PriceProviderUpdated(_priceProvider: address) <<VaultStorage>> -    <<event>> AllocateThresholdUpdated(_threshold: uint256) <<VaultStorage>> -    <<event>> RebaseThresholdUpdated(_threshold: uint256) <<VaultStorage>> -    <<event>> StrategistUpdated(_address: address) <<VaultStorage>> -    <<event>> MaxSupplyDiffChanged(maxSupplyDiff: uint256) <<VaultStorage>> -    <<event>> YieldDistribution(_to: address, _yield: uint256, _fee: uint256) <<VaultStorage>> -    <<event>> TrusteeFeeBpsChanged(_basis: uint256) <<VaultStorage>> -    <<event>> TrusteeAddressChanged(_address: address) <<VaultStorage>> -    <<event>> NetOusdMintForStrategyThresholdChanged(_threshold: uint256) <<VaultStorage>> -    <<event>> SwapperChanged(_address: address) <<VaultStorage>> -    <<event>> SwapAllowedUndervalueChanged(_basis: uint256) <<VaultStorage>> -    <<event>> SwapSlippageChanged(_asset: address, _basis: uint256) <<VaultStorage>> -    <<event>> Swapped(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _toAssetAmount: uint256) <<VaultStorage>> -    <<event>> DripperChanged(_dripper: address) <<VaultStorage>> -    <<event>> StrategyAddedToMintWhitelist(strategy: address) <<VaultStorage>> -    <<event>> StrategyRemovedFromMintWhitelist(strategy: address) <<VaultStorage>> -    <<event>> WithdrawalRequested(_withdrawer: address, _requestId: uint256, _amount: uint256, _queued: uint256) <<VaultStorage>> -    <<event>> WithdrawalClaimed(_withdrawer: address, _requestId: uint256, _amount: uint256) <<VaultStorage>> -    <<event>> WithdrawalClaimable(_claimable: uint256, _newClaimable: uint256) <<VaultStorage>> +287 + +OETHVaultCore +../contracts/vault/OETHVaultCore.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _deprecated_rebaseHooksAddr: address <<VaultStorage>> +   _deprecated_uniswapAddr: address <<VaultStorage>> +   _deprecated_swapTokens: address[] <<VaultStorage>> +   __gap: uint256[44] <<VaultStorage>> +   __gap: uint256[50] <<OETHVaultCore>> +Internal: +   assets: mapping(address=>Asset) <<VaultStorage>> +   allAssets: address[] <<VaultStorage>> +   strategies: mapping(address=>Strategy) <<VaultStorage>> +   allStrategies: address[] <<VaultStorage>> +   oUSD: OUSD <<VaultStorage>> +   swapConfig: SwapConfig <<VaultStorage>> +   MAX_INT: uint256 <<VaultCore>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   priceProvider: address <<VaultStorage>> +   rebasePaused: bool <<VaultStorage>> +   capitalPaused: bool <<VaultStorage>> +   redeemFeeBps: uint256 <<VaultStorage>> +   vaultBuffer: uint256 <<VaultStorage>> +   autoAllocateThreshold: uint256 <<VaultStorage>> +   rebaseThreshold: uint256 <<VaultStorage>> +   adminImplPosition: bytes32 <<VaultStorage>> +   strategistAddr: address <<VaultStorage>> +   assetDefaultStrategies: mapping(address=>address) <<VaultStorage>> +   maxSupplyDiff: uint256 <<VaultStorage>> +   trusteeAddress: address <<VaultStorage>> +   trusteeFeeBps: uint256 <<VaultStorage>> +   MINT_MINIMUM_UNIT_PRICE: uint256 <<VaultStorage>> +   ousdMetaStrategy: address <<VaultStorage>> +   netOusdMintedForStrategy: int256 <<VaultStorage>> +   netOusdMintForStrategyThreshold: uint256 <<VaultStorage>> +   MIN_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   MAX_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   isMintWhitelistedStrategy: mapping(address=>bool) <<VaultStorage>> +   dripper: address <<VaultStorage>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<VaultStorage>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<VaultStorage>> +   withdrawalClaimDelay: uint256 <<VaultStorage>> +   weth: address <<OETHVaultCore>> +   wethAssetIndex: uint256 <<OETHVaultCore>> + +Private: +    abs(x: int256): uint256 <<VaultCore>> +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _mint(_asset: address, _amount: uint256, _minimumOusdAmount: uint256) <<OETHVaultCore>> +    _redeem(_amount: uint256, _minimumUnitAmount: uint256) <<OETHVaultCore>> +    _postRedeem(_amount: uint256) <<VaultCore>> +    _allocate() <<OETHVaultCore>> +    _rebase(): uint256 <<whenNotRebasePaused>> <<VaultCore>> +    _totalValue(): (value: uint256) <<OETHVaultCore>> +    _totalValueInVault(): (value: uint256) <<OETHVaultCore>> +    _totalValueInStrategies(): (value: uint256) <<VaultCore>> +    _totalValueInStrategy(_strategyAddr: address): (value: uint256) <<VaultCore>> +    _checkBalance(_asset: address): (balance: uint256) <<OETHVaultCore>> +    _calculateRedeemOutputs(_amount: uint256): (outputs: uint256[]) <<OETHVaultCore>> +    _toUnits(_raw: uint256, _asset: address): uint256 <<VaultCore>> +    _toUnitPrice(_asset: address, isMint: bool): (price: uint256) <<VaultCore>> +    _getDecimals(_asset: address): (decimals: uint256) <<VaultCore>> +    _claimWithdrawal(requestId: uint256): (amount: uint256) <<OETHVaultCore>> +    _addWithdrawalQueueLiquidity(): (addedClaimable: uint256) <<OETHVaultCore>> +    _wethAvailable(): (wethAvailable: uint256) <<OETHVaultCore>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>> +    initialize(_priceProvider: address, _oToken: address) <<onlyGovernor, initializer>> <<VaultInitializer>> +    mint(_asset: address, _amount: uint256, _minimumOusdAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> +    mintForStrategy(amount: uint256) <<whenNotCapitalPaused>> <<OETHVaultCore>> +    redeem(_amount: uint256, _minimumUnitAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> +    burnForStrategy(amount: uint256) <<whenNotCapitalPaused>> <<OETHVaultCore>> +    redeemAll(_minimumUnitAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> +    allocate() <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> +    rebase() <<nonReentrant>> <<VaultCore>> +    totalValue(): (value: uint256) <<VaultCore>> +    checkBalance(_asset: address): uint256 <<VaultCore>> +    calculateRedeemOutputs(_amount: uint256): uint256[] <<VaultCore>> +    priceUnitMint(asset: address): (price: uint256) <<VaultCore>> +    priceUnitRedeem(asset: address): (price: uint256) <<VaultCore>> +    getAllAssets(): address[] <<VaultCore>> +    getStrategyCount(): uint256 <<VaultCore>> +    getAllStrategies(): address[] <<VaultCore>> +    isSupportedAsset(_asset: address): bool <<VaultCore>> +    null() <<VaultCore>> +    cacheWETHAssetIndex() <<onlyGovernor>> <<OETHVaultCore>> +    requestWithdrawal(_amount: uint256): (requestId: uint256, queued: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> +    claimWithdrawal(_requestId: uint256): (amount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> +    claimWithdrawals(_requestIds: uint256[]): (amounts: uint256[], totalAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>> +    addWithdrawalQueueLiquidity() <<OETHVaultCore>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> AssetSupported(_asset: address) <<VaultStorage>> +    <<event>> AssetRemoved(_asset: address) <<VaultStorage>> +    <<event>> AssetDefaultStrategyUpdated(_asset: address, _strategy: address) <<VaultStorage>> +    <<event>> AssetAllocated(_asset: address, _strategy: address, _amount: uint256) <<VaultStorage>> +    <<event>> StrategyApproved(_addr: address) <<VaultStorage>> +    <<event>> StrategyRemoved(_addr: address) <<VaultStorage>> +    <<event>> Mint(_addr: address, _value: uint256) <<VaultStorage>> +    <<event>> Redeem(_addr: address, _value: uint256) <<VaultStorage>> +    <<event>> CapitalPaused() <<VaultStorage>> +    <<event>> CapitalUnpaused() <<VaultStorage>> +    <<event>> RebasePaused() <<VaultStorage>> +    <<event>> RebaseUnpaused() <<VaultStorage>> +    <<event>> VaultBufferUpdated(_vaultBuffer: uint256) <<VaultStorage>> +    <<event>> OusdMetaStrategyUpdated(_ousdMetaStrategy: address) <<VaultStorage>> +    <<event>> RedeemFeeUpdated(_redeemFeeBps: uint256) <<VaultStorage>> +    <<event>> PriceProviderUpdated(_priceProvider: address) <<VaultStorage>> +    <<event>> AllocateThresholdUpdated(_threshold: uint256) <<VaultStorage>> +    <<event>> RebaseThresholdUpdated(_threshold: uint256) <<VaultStorage>> +    <<event>> StrategistUpdated(_address: address) <<VaultStorage>> +    <<event>> MaxSupplyDiffChanged(maxSupplyDiff: uint256) <<VaultStorage>> +    <<event>> YieldDistribution(_to: address, _yield: uint256, _fee: uint256) <<VaultStorage>> +    <<event>> TrusteeFeeBpsChanged(_basis: uint256) <<VaultStorage>> +    <<event>> TrusteeAddressChanged(_address: address) <<VaultStorage>> +    <<event>> NetOusdMintForStrategyThresholdChanged(_threshold: uint256) <<VaultStorage>> +    <<event>> SwapperChanged(_address: address) <<VaultStorage>> +    <<event>> SwapAllowedUndervalueChanged(_basis: uint256) <<VaultStorage>> +    <<event>> SwapSlippageChanged(_asset: address, _basis: uint256) <<VaultStorage>> +    <<event>> Swapped(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _toAssetAmount: uint256) <<VaultStorage>> +    <<event>> StrategyAddedToMintWhitelist(strategy: address) <<VaultStorage>> +    <<event>> StrategyRemovedFromMintWhitelist(strategy: address) <<VaultStorage>> +    <<event>> DripperChanged(_dripper: address) <<VaultStorage>> +    <<event>> WithdrawalRequested(_withdrawer: address, _requestId: uint256, _amount: uint256, _queued: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimed(_withdrawer: address, _requestId: uint256, _amount: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimable(_claimable: uint256, _newClaimable: uint256) <<VaultStorage>> +    <<event>> WithdrawalClaimDelayUpdated(_newDelay: uint256) <<VaultStorage>>    <<modifier>> initializer() <<Initializable>>    <<modifier>> onlyGovernor() <<Governable>>    <<modifier>> nonReentrant() <<Governable>> diff --git a/contracts/docs/OETHVaultHierarchy.svg b/contracts/docs/OETHVaultHierarchy.svg deleted file mode 100644 index ce6caafd6c..0000000000 --- a/contracts/docs/OETHVaultHierarchy.svg +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - -UmlClassDiagram - - - -20 - -Governable -../contracts/governance/Governable.sol - - - -215 - -OUSD -../contracts/token/OUSD.sol - - - -215->20 - - - - - -225 - -<<Abstract>> -Initializable -../contracts/utils/Initializable.sol - - - -215->225 - - - - - -228 - -<<Abstract>> -InitializableERC20Detailed -../contracts/utils/InitializableERC20Detailed.sol - - - -215->228 - - - - - -233 - -OETHVaultAdmin -../contracts/vault/OETHVaultAdmin.sol - - - -237 - -VaultAdmin -../contracts/vault/VaultAdmin.sol - - - -233->237 - - - - - -234 - -OETHVaultCore -../contracts/vault/OETHVaultCore.sol - - - -238 - -VaultCore -../contracts/vault/VaultCore.sol - - - -234->238 - - - - - -240 - -VaultStorage -../contracts/vault/VaultStorage.sol - - - -237->240 - - - - - -239 - -VaultInitializer -../contracts/vault/VaultInitializer.sol - - - -238->239 - - - - - -239->215 - - - - - -239->240 - - - - - -240->20 - - - - - -240->215 - - - - - -240->225 - - - - - diff --git a/contracts/docs/OETHVaultStorage.svg b/contracts/docs/OETHVaultStorage.svg index 8fb932c367..e425f8712d 100644 --- a/contracts/docs/OETHVaultStorage.svg +++ b/contracts/docs/OETHVaultStorage.svg @@ -4,268 +4,272 @@ - - + + StorageDiagram - + 8 - -OETHVaultCore <<Contract>> - -slot - -0 - -1-50 - -51 - -52 - -53 - -54 - -55 - -56 - -57 - -58 - -59 - -60 - -61 - -62 - -63 - -64 - -65 - -66 - -67 - -68 - -69 - -70 - -71 - -72 - -73 - -74 - -75-76 - -77 - -78-122 - -123 - -124-173 - -type: <inherited contract>.variable (bytes) - -unallocated (30) - -bool: Initializable.initializing (1) - -bool: Initializable.initialized (1) - -uint256[50]: Initializable.______gap (1600) - -mapping(address=>Asset): VaultStorage.assets (32) - -address[]: VaultStorage.allAssets (32) - -mapping(address=>Strategy): VaultStorage.strategies (32) - -address[]: VaultStorage.allStrategies (32) - -unallocated (10) - -bool: VaultStorage.capitalPaused (1) - -bool: VaultStorage.rebasePaused (1) - -address: VaultStorage.priceProvider (20) - -uint256: VaultStorage.redeemFeeBps (32) - -uint256: VaultStorage.vaultBuffer (32) - -uint256: VaultStorage.autoAllocateThreshold (32) - -uint256: VaultStorage.rebaseThreshold (32) - -unallocated (12) - -OUSD: VaultStorage.oUSD (20) - -unallocated (12) - -address: VaultStorage._deprecated_rebaseHooksAddr (20) - -unallocated (12) - -address: VaultStorage._deprecated_uniswapAddr (20) - -unallocated (12) - -address: VaultStorage.strategistAddr (20) - -mapping(address=>address): VaultStorage.assetDefaultStrategies (32) - -uint256: VaultStorage.maxSupplyDiff (32) - -unallocated (12) - -address: VaultStorage.trusteeAddress (20) - -uint256: VaultStorage.trusteeFeeBps (32) - -address[]: VaultStorage._deprecated_swapTokens (32) - -unallocated (12) - -address: VaultStorage.ousdMetaStrategy (20) - -int256: VaultStorage.netOusdMintedForStrategy (32) - -uint256: VaultStorage.netOusdMintForStrategyThreshold (32) - -SwapConfig: VaultStorage.swapConfig (32) - -mapping(address=>bool): VaultStorage.isMintWhitelistedStrategy (32) - -unallocated (12) - -address: VaultStorage.dripper (20) - -WithdrawalQueueMetadata: VaultStorage.withdrawalQueueMetadata (64) - -mapping(uint256=>WithdrawalRequest): VaultStorage.withdrawalRequests (32) - -uint256[45]: VaultStorage.__gap (1440) - -uint256: wethAssetIndex (32) - -uint256[50]: __gap (1600) + +OETHVaultCore <<Contract>> + +slot + +0 + +1-50 + +51 + +52 + +53 + +54 + +55 + +56 + +57 + +58 + +59 + +60 + +61 + +62 + +63 + +64 + +65 + +66 + +67 + +68 + +69 + +70 + +71 + +72 + +73 + +74 + +75-76 + +77 + +78 + +79-122 + +123 + +124-173 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.______gap (1600) + +mapping(address=>Asset): VaultStorage.assets (32) + +address[]: VaultStorage.allAssets (32) + +mapping(address=>Strategy): VaultStorage.strategies (32) + +address[]: VaultStorage.allStrategies (32) + +unallocated (10) + +bool: VaultStorage.capitalPaused (1) + +bool: VaultStorage.rebasePaused (1) + +address: VaultStorage.priceProvider (20) + +uint256: VaultStorage.redeemFeeBps (32) + +uint256: VaultStorage.vaultBuffer (32) + +uint256: VaultStorage.autoAllocateThreshold (32) + +uint256: VaultStorage.rebaseThreshold (32) + +unallocated (12) + +OUSD: VaultStorage.oUSD (20) + +unallocated (12) + +address: VaultStorage._deprecated_rebaseHooksAddr (20) + +unallocated (12) + +address: VaultStorage._deprecated_uniswapAddr (20) + +unallocated (12) + +address: VaultStorage.strategistAddr (20) + +mapping(address=>address): VaultStorage.assetDefaultStrategies (32) + +uint256: VaultStorage.maxSupplyDiff (32) + +unallocated (12) + +address: VaultStorage.trusteeAddress (20) + +uint256: VaultStorage.trusteeFeeBps (32) + +address[]: VaultStorage._deprecated_swapTokens (32) + +unallocated (12) + +address: VaultStorage.ousdMetaStrategy (20) + +int256: VaultStorage.netOusdMintedForStrategy (32) + +uint256: VaultStorage.netOusdMintForStrategyThreshold (32) + +SwapConfig: VaultStorage.swapConfig (32) + +mapping(address=>bool): VaultStorage.isMintWhitelistedStrategy (32) + +unallocated (12) + +address: VaultStorage.dripper (20) + +WithdrawalQueueMetadata: VaultStorage.withdrawalQueueMetadata (64) + +mapping(uint256=>WithdrawalRequest): VaultStorage.withdrawalRequests (32) + +uint256: VaultStorage.withdrawalClaimDelay (32) + +uint256[44]: VaultStorage.__gap (1408) + +uint256: wethAssetIndex (32) + +uint256[50]: __gap (1600) 1 - -Asset <<Struct>> - -offset - -0 - -type: variable (bytes) - -unallocated (27) - -uint16: allowedOracleSlippageBps (2) - -uint8: decimals (1) - -UnitConversion: unitConversion (1) - -bool: isSupported (1) + +Asset <<Struct>> + +offset + +0 + +type: variable (bytes) + +unallocated (27) + +uint16: allowedOracleSlippageBps (2) + +uint8: decimals (1) + +UnitConversion: unitConversion (1) + +bool: isSupported (1) 8:8->1 - - + + 2 - -address[]: allAssets <<Array>> -0x46bddb1178e94d7f2892ff5f366840eb658911794f2c3a44c450aa2c505186c1 - -offset - -0 - -type: variable (bytes) - -unallocated (12) - -address (20) + +address[]: allAssets <<Array>> +0x46bddb1178e94d7f2892ff5f366840eb658911794f2c3a44c450aa2c505186c1 + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) 8:10->2 - - + + 3 - -Strategy <<Struct>> - -offset - -0 - -1 - -type: variable (bytes) - -unallocated (31) - -bool: isSupported (1) - -uint256: _deprecated (32) + +Strategy <<Struct>> + +offset + +0 + +1 + +type: variable (bytes) + +unallocated (31) + +bool: isSupported (1) + +uint256: _deprecated (32) 8:13->3 - - + + 4 - -address[]: allStrategies <<Array>> -0x4a11f94e20a93c79f6ec743a1954ec4fc2c08429ae2122118bf234b2185c81b8 - -offset - -0 - -type: variable (bytes) - -unallocated (12) - -address (20) + +address[]: allStrategies <<Array>> +0x4a11f94e20a93c79f6ec743a1954ec4fc2c08429ae2122118bf234b2185c81b8 + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) 8:15->4 - - + + @@ -288,8 +292,8 @@ 8:37->5 - - + + @@ -348,8 +352,8 @@ 8:50->7 - - + + diff --git a/contracts/docs/OSonicVaultAdminSquashed.svg b/contracts/docs/OSonicVaultAdminSquashed.svg index 22be0c6cef..ba51485b4e 100644 --- a/contracts/docs/OSonicVaultAdminSquashed.svg +++ b/contracts/docs/OSonicVaultAdminSquashed.svg @@ -4,116 +4,118 @@ - - + + UmlClassDiagram - - + + -273 - -OSonicVaultAdmin -../contracts/vault/OSonicVaultAdmin.sol - -Private: -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   ______gap: uint256[50] <<Initializable>> -   governorPosition: bytes32 <<Governable>> -   pendingGovernorPosition: bytes32 <<Governable>> -   reentryStatusPosition: bytes32 <<Governable>> -   _deprecated_rebaseHooksAddr: address <<VaultStorage>> -   _deprecated_uniswapAddr: address <<VaultStorage>> -   _deprecated_swapTokens: address[] <<VaultStorage>> -   __gap: uint256[44] <<VaultStorage>> -Internal: -   assets: mapping(address=>Asset) <<VaultStorage>> -   allAssets: address[] <<VaultStorage>> -   strategies: mapping(address=>Strategy) <<VaultStorage>> -   allStrategies: address[] <<VaultStorage>> -   oUSD: OUSD <<VaultStorage>> -   swapConfig: SwapConfig <<VaultStorage>> -Public: -   _NOT_ENTERED: uint256 <<Governable>> -   _ENTERED: uint256 <<Governable>> -   priceProvider: address <<VaultStorage>> -   rebasePaused: bool <<VaultStorage>> -   capitalPaused: bool <<VaultStorage>> -   redeemFeeBps: uint256 <<VaultStorage>> -   vaultBuffer: uint256 <<VaultStorage>> -   autoAllocateThreshold: uint256 <<VaultStorage>> -   rebaseThreshold: uint256 <<VaultStorage>> -   adminImplPosition: bytes32 <<VaultStorage>> -   strategistAddr: address <<VaultStorage>> -   assetDefaultStrategies: mapping(address=>address) <<VaultStorage>> -   maxSupplyDiff: uint256 <<VaultStorage>> -   trusteeAddress: address <<VaultStorage>> -   trusteeFeeBps: uint256 <<VaultStorage>> -   MINT_MINIMUM_UNIT_PRICE: uint256 <<VaultStorage>> -   ousdMetaStrategy: address <<VaultStorage>> -   netOusdMintedForStrategy: int256 <<VaultStorage>> -   netOusdMintForStrategyThreshold: uint256 <<VaultStorage>> -   MIN_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> -   MAX_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> -   isMintWhitelistedStrategy: mapping(address=>bool) <<VaultStorage>> -   dripper: address <<VaultStorage>> -   withdrawalQueueMetadata: WithdrawalQueueMetadata <<VaultStorage>> -   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<VaultStorage>> -   withdrawalClaimDelay: uint256 <<VaultStorage>> -   weth: address <<OETHVaultAdmin>> - -Internal: -    _governor(): (governorOut: address) <<Governable>> -    _pendingGovernor(): (pendingGovernor: address) <<Governable>> -    _setGovernor(newGovernor: address) <<Governable>> -    _setPendingGovernor(newGovernor: address) <<Governable>> -    _changeGovernor(_newGovernor: address) <<Governable>> -    _swapCollateral(address, address, uint256, uint256, bytes): uint256 <<OETHVaultAdmin>> -    _depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> -    _withdrawFromStrategy(_recipient: address, _strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> -    _withdrawAllFromStrategy(_strategyAddr: address) <<OETHVaultAdmin>> -    _withdrawAllFromStrategies() <<OETHVaultAdmin>> -    _cacheDecimals(token: address) <<VaultAdmin>> -    _wethAvailable(): (wethAvailable: uint256) <<OETHVaultAdmin>> -External: -    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> -    claimGovernance() <<Governable>> -    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>> -    setPriceProvider(_priceProvider: address) <<onlyGovernor>> <<VaultAdmin>> -    setRedeemFeeBps(_redeemFeeBps: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setVaultBuffer(_vaultBuffer: uint256) <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    setAutoAllocateThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setRebaseThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setStrategistAddr(_address: address) <<onlyGovernor>> <<VaultAdmin>> -    setAssetDefaultStrategy(_asset: address, _strategy: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    setNetOusdMintForStrategyThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setDripper(_dripper: address) <<onlyGovernor>> <<VaultAdmin>> -    setWithdrawalClaimDelay(_delay: uint256) <<onlyGovernor>> <<VaultAdmin>> -    swapCollateral(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _minToAssetAmount: uint256, _data: bytes): (toAssetAmount: uint256) <<nonReentrant, onlyGovernorOrStrategist>> <<VaultAdmin>> -    setSwapper(_swapperAddr: address) <<onlyGovernor>> <<VaultAdmin>> -    swapper(): (swapper_: address) <<VaultAdmin>> -    setSwapAllowedUndervalue(_basis: uint16) <<onlyGovernor>> <<VaultAdmin>> -    allowedSwapUndervalue(): (value: uint256) <<VaultAdmin>> -    setOracleSlippage(_asset: address, _allowedOracleSlippageBps: uint16) <<onlyGovernor>> <<VaultAdmin>> -    supportAsset(_asset: address, _unitConversion: uint8) <<onlyGovernor>> <<OSonicVaultAdmin>> -    removeAsset(_asset: address) <<onlyGovernor>> <<VaultAdmin>> -    cacheDecimals(_asset: address) <<onlyGovernor>> <<VaultAdmin>> -    approveStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> -    removeStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> -    depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> -    withdrawFromStrategy(_strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> -    setMaxSupplyDiff(_maxSupplyDiff: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setTrusteeAddress(_address: address) <<onlyGovernor>> <<VaultAdmin>> -    setTrusteeFeeBps(_basis: uint256) <<onlyGovernor>> <<VaultAdmin>> -    setOusdMetaStrategy(_ousdMetaStrategy: address) <<onlyGovernor>> <<VaultAdmin>> -    pauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    unpauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    pauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    unpauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<VaultAdmin>> -    withdrawAllFromStrategy(_strategyAddr: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> -    withdrawAllFromStrategies() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +289 + +OSonicVaultAdmin +../contracts/vault/OSonicVaultAdmin.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _deprecated_rebaseHooksAddr: address <<VaultStorage>> +   _deprecated_uniswapAddr: address <<VaultStorage>> +   _deprecated_swapTokens: address[] <<VaultStorage>> +   __gap: uint256[44] <<VaultStorage>> +Internal: +   assets: mapping(address=>Asset) <<VaultStorage>> +   allAssets: address[] <<VaultStorage>> +   strategies: mapping(address=>Strategy) <<VaultStorage>> +   allStrategies: address[] <<VaultStorage>> +   oUSD: OUSD <<VaultStorage>> +   swapConfig: SwapConfig <<VaultStorage>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   priceProvider: address <<VaultStorage>> +   rebasePaused: bool <<VaultStorage>> +   capitalPaused: bool <<VaultStorage>> +   redeemFeeBps: uint256 <<VaultStorage>> +   vaultBuffer: uint256 <<VaultStorage>> +   autoAllocateThreshold: uint256 <<VaultStorage>> +   rebaseThreshold: uint256 <<VaultStorage>> +   adminImplPosition: bytes32 <<VaultStorage>> +   strategistAddr: address <<VaultStorage>> +   assetDefaultStrategies: mapping(address=>address) <<VaultStorage>> +   maxSupplyDiff: uint256 <<VaultStorage>> +   trusteeAddress: address <<VaultStorage>> +   trusteeFeeBps: uint256 <<VaultStorage>> +   MINT_MINIMUM_UNIT_PRICE: uint256 <<VaultStorage>> +   ousdMetaStrategy: address <<VaultStorage>> +   netOusdMintedForStrategy: int256 <<VaultStorage>> +   netOusdMintForStrategyThreshold: uint256 <<VaultStorage>> +   MIN_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   MAX_UNIT_PRICE_DRIFT: uint256 <<VaultStorage>> +   isMintWhitelistedStrategy: mapping(address=>bool) <<VaultStorage>> +   dripper: address <<VaultStorage>> +   withdrawalQueueMetadata: WithdrawalQueueMetadata <<VaultStorage>> +   withdrawalRequests: mapping(uint256=>WithdrawalRequest) <<VaultStorage>> +   withdrawalClaimDelay: uint256 <<VaultStorage>> +   weth: address <<OETHVaultAdmin>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _swapCollateral(address, address, uint256, uint256, bytes): uint256 <<OETHVaultAdmin>> +    _depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> +    _withdrawFromStrategy(_recipient: address, _strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<OETHVaultAdmin>> +    _withdrawAllFromStrategy(_strategyAddr: address) <<OETHVaultAdmin>> +    _withdrawAllFromStrategies() <<OETHVaultAdmin>> +    _cacheDecimals(token: address) <<VaultAdmin>> +    _wethAvailable(): (wethAvailable: uint256) <<OETHVaultAdmin>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>> +    setPriceProvider(_priceProvider: address) <<onlyGovernor>> <<VaultAdmin>> +    setRedeemFeeBps(_redeemFeeBps: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setVaultBuffer(_vaultBuffer: uint256) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    setAutoAllocateThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setRebaseThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setStrategistAddr(_address: address) <<onlyGovernor>> <<VaultAdmin>> +    setAssetDefaultStrategy(_asset: address, _strategy: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    setNetOusdMintForStrategyThreshold(_threshold: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setDripper(_dripper: address) <<onlyGovernor>> <<VaultAdmin>> +    setWithdrawalClaimDelay(_delay: uint256) <<onlyGovernor>> <<VaultAdmin>> +    swapCollateral(_fromAsset: address, _toAsset: address, _fromAssetAmount: uint256, _minToAssetAmount: uint256, _data: bytes): (toAssetAmount: uint256) <<nonReentrant, onlyGovernorOrStrategist>> <<VaultAdmin>> +    setSwapper(_swapperAddr: address) <<onlyGovernor>> <<VaultAdmin>> +    swapper(): (swapper_: address) <<VaultAdmin>> +    setSwapAllowedUndervalue(_basis: uint16) <<onlyGovernor>> <<VaultAdmin>> +    allowedSwapUndervalue(): (value: uint256) <<VaultAdmin>> +    setOracleSlippage(_asset: address, _allowedOracleSlippageBps: uint16) <<onlyGovernor>> <<VaultAdmin>> +    supportAsset(_asset: address, _unitConversion: uint8) <<onlyGovernor>> <<OSonicVaultAdmin>> +    removeAsset(_asset: address) <<onlyGovernor>> <<VaultAdmin>> +    cacheDecimals(_asset: address) <<onlyGovernor>> <<VaultAdmin>> +    approveStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> +    removeStrategy(_addr: address) <<onlyGovernor>> <<VaultAdmin>> +    depositToStrategy(_strategyToAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> +    withdrawFromStrategy(_strategyFromAddress: address, _assets: address[], _amounts: uint256[]) <<onlyGovernorOrStrategist, nonReentrant>> <<VaultAdmin>> +    setMaxSupplyDiff(_maxSupplyDiff: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setTrusteeAddress(_address: address) <<onlyGovernor>> <<VaultAdmin>> +    setTrusteeFeeBps(_basis: uint256) <<onlyGovernor>> <<VaultAdmin>> +    setOusdMetaStrategy(_ousdMetaStrategy: address) <<onlyGovernor>> <<VaultAdmin>> +    pauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    unpauseRebase() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    pauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    unpauseCapital() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<VaultAdmin>> +    withdrawAllFromStrategy(_strategyAddr: address) <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    withdrawAllFromStrategies() <<onlyGovernorOrStrategist>> <<VaultAdmin>> +    addStrategyToMintWhitelist(strategyAddr: address) <<onlyGovernor>> <<OETHVaultAdmin>> +    removeStrategyFromMintWhitelist(strategyAddr: address) <<onlyGovernor>> <<OETHVaultAdmin>> Public:    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>>    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> diff --git a/contracts/docs/OSonicVaultCoreSquashed.svg b/contracts/docs/OSonicVaultCoreSquashed.svg index 51ac9515a6..9d467a3367 100644 --- a/contracts/docs/OSonicVaultCoreSquashed.svg +++ b/contracts/docs/OSonicVaultCoreSquashed.svg @@ -9,9 +9,9 @@ UmlClassDiagram - + -274 +290 OSonicVaultCore ../contracts/vault/OSonicVaultCore.sol @@ -97,9 +97,9 @@    setAdminImpl(newImpl: address) <<onlyGovernor>> <<VaultStorage>>    initialize(_priceProvider: address, _oToken: address) <<onlyGovernor, initializer>> <<VaultInitializer>>    mint(_asset: address, _amount: uint256, _minimumOusdAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>> -    mintForStrategy(_amount: uint256) <<whenNotCapitalPaused, onlyOusdMetaStrategy>> <<VaultCore>> +    mintForStrategy(amount: uint256) <<whenNotCapitalPaused>> <<OETHVaultCore>>    redeem(uint256, uint256) <<OSonicVaultCore>> -    burnForStrategy(_amount: uint256) <<whenNotCapitalPaused, onlyOusdMetaStrategy>> <<VaultCore>> +    burnForStrategy(amount: uint256) <<whenNotCapitalPaused>> <<OETHVaultCore>>    redeemAll(_minimumUnitAmount: uint256) <<whenNotCapitalPaused, nonReentrant>> <<VaultCore>>    allocate() <<whenNotCapitalPaused, nonReentrant>> <<OETHVaultCore>>    rebase() <<nonReentrant>> <<VaultCore>> diff --git a/contracts/docs/VaultHierarchy.svg b/contracts/docs/VaultHierarchy.svg index 044158df15..2954412d6e 100644 --- a/contracts/docs/VaultHierarchy.svg +++ b/contracts/docs/VaultHierarchy.svg @@ -4,128 +4,180 @@ - - + + UmlClassDiagram - - + + -20 - -Governable -../contracts/governance/Governable.sol +21 + +Governable +../contracts/governance/Governable.sol - + -215 - -OUSD -../contracts/token/OUSD.sol +261 + +OUSD +../contracts/token/OUSD.sol - - -215->20 - - + + +261->21 + + - + -225 - -<<Abstract>> -Initializable -../contracts/utils/Initializable.sol - - - -215->225 - - +277 + +<<Abstract>> +Initializable +../contracts/utils/Initializable.sol - + -228 - -<<Abstract>> -InitializableERC20Detailed -../contracts/utils/InitializableERC20Detailed.sol +282 + +OETHBaseVaultAdmin +../contracts/vault/OETHBaseVaultAdmin.sol + + + +286 + +OETHVaultAdmin +../contracts/vault/OETHVaultAdmin.sol - + -215->228 - - +282->286 + + - + -237 - -VaultAdmin -../contracts/vault/VaultAdmin.sol +283 + +OETHBaseVaultCore +../contracts/vault/OETHBaseVaultCore.sol - - -240 - -VaultStorage -../contracts/vault/VaultStorage.sol - - - -237->240 - - - - - -238 - -VaultCore -../contracts/vault/VaultCore.sol - - + -239 - -VaultInitializer -../contracts/vault/VaultInitializer.sol +287 + +OETHVaultCore +../contracts/vault/OETHVaultCore.sol - + + +283->287 + + + + + +293 + +VaultAdmin +../contracts/vault/VaultAdmin.sol + + + +286->293 + + + + + +294 + +VaultCore +../contracts/vault/VaultCore.sol + + -238->239 - - +287->294 + + - - -239->215 - - + + +289 + +OSonicVaultAdmin +../contracts/vault/OSonicVaultAdmin.sol - + -239->240 - - - - +289->286 + + + + + +290 + +OSonicVaultCore +../contracts/vault/OSonicVaultCore.sol + + + +290->287 + + + + + +296 + +VaultStorage +../contracts/vault/VaultStorage.sol + + + +293->296 + + + + + +295 + +VaultInitializer +../contracts/vault/VaultInitializer.sol + + -240->20 - - +294->295 + + - + -240->215 - - - - - -240->225 - - +295->296 + + + + + +296->21 + + + + + +296->261 + + + + + +296->277 + + diff --git a/contracts/docs/generate.sh b/contracts/docs/generate.sh index 77e5aef682..3605bf3e73 100644 --- a/contracts/docs/generate.sh +++ b/contracts/docs/generate.sh @@ -29,6 +29,10 @@ sol2uml .. -v -hv -hf -he -hs -hl -b OETHHarvester -o OETHHarvesterHierarchy.svg sol2uml .. -s -d 0 -b OETHHarvester -o OETHHarvesterSquashed.svg sol2uml storage .. -c OETHHarvester -o OETHHarvesterStorage.svg +sol2uml .. -v -hv -hf -he -hs -hl -b OETHHarvesterSimple -o OETHHarvesterSimpleHierarchy.svg +sol2uml .. -s -d 0 -b OETHHarvesterSimple -o OETHHarvesterSimpleSquashed.svg +sol2uml storage .. -c OETHHarvesterSimple -o OETHHarvesterSimpleStorage.svg --hideExpand __gap,___gap,______gap + # contracts/governance sol2uml .. -v -hv -hf -he -hs -hl -b Governor -o GovernorHierarchy.svg sol2uml .. -s -d 0 -b Governor -o GovernorSquashed.svg @@ -156,17 +160,21 @@ sol2uml .. -s -d 0 -b WOSonic -o WOSonicSquashed.svg sol2uml storage .. -c WOSonic -o WOSonicStorage.svg --hideExpand ______gap # contracts/vault -sol2uml .. -v -hv -hf -he -hs -hl -hi -b VaultCore,VaultAdmin -o VaultHierarchy.svg + +sol2uml .. -v -hv -hf -he -hs -hl -hi -b OETHBaseVaultCore,OETHBaseVaultAdmin,OSonicVaultCore,OSonicVaultAdmin -o VaultHierarchy.svg + sol2uml .. -s -d 0 -b VaultCore -o VaultCoreSquashed.svg sol2uml .. -s -d 0 -b VaultAdmin -o VaultAdminSquashed.svg sol2uml storage .. -c VaultCore -o VaultStorage.svg --hideExpand __gap,______gap,_deprecated_swapTokens -sol2uml .. -v -hv -hf -he -hs -hl -hi -b OETHVaultCore,OETHVaultAdmin -o OETHVaultHierarchy.svg sol2uml .. -s -d 0 -b OETHVaultCore -o OETHVaultCoreSquashed.svg sol2uml .. -s -d 0 -b OETHVaultAdmin -o OETHVaultAdminSquashed.svg sol2uml storage .. -c OETHVaultCore -o OETHVaultStorage.svg --hideExpand __gap,______gap,_deprecated_swapTokens -sol2uml .. -v -hv -hf -he -hs -hl -hi -b OSonicVaultCore,OSonicVaultAdmin -o OSonicVaultHierarchy.svg +sol2uml .. -s -d 0 -b OETHBaseVaultCore -o OETHBaseVaultCoreSquashed.svg +sol2uml .. -s -d 0 -b OETHBaseVaultAdmin -o OETHBaseVaultAdminSquashed.svg +sol2uml storage .. -c OETHBaseVaultCore -o OETHBaseVaultStorage.svg --hideExpand __gap,______gap + sol2uml .. -s -d 0 -b OSonicVaultCore -o OSonicVaultCoreSquashed.svg sol2uml .. -s -d 0 -b OSonicVaultAdmin -o OSonicVaultAdminSquashed.svg sol2uml storage .. -c OSonicVaultCore -o OSonicVaultStorage.svg --hideExpand __gap,______gap,_deprecated_swapTokens From 397ce1d0543072744b4a83a6f674207d49dffa36 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 13 Feb 2025 12:41:22 +1100 Subject: [PATCH 08/80] WIP Sonic Curve AMO fork tests --- contracts/test/_fixture-sonic.js | 36 + .../base/aerodrome-amo.base.fork-test.js | 2 +- .../bridged-woeth-strategy.base.fork-test.js | 2 +- .../base/bridged-woeth-strategy.base.js | 2 +- .../base/curve-amo.base.fork-test.js | 2 +- .../sonic/curve-amo.sonic.fork-test.js | 737 ++++++++++++++++++ .../sonicStaking.sonic.fork-test.js | 8 +- 7 files changed, 781 insertions(+), 8 deletions(-) create mode 100644 contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js rename contracts/test/strategies/{ => sonic}/sonicStaking.sonic.fork-test.js (68%) diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index e7ae51a170..6ffb455558 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -7,6 +7,11 @@ const { nodeRevert, nodeSnapshot } = require("./_fixture"); const addresses = require("../utils/addresses"); const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); +const erc20Abi = require("./abi/erc20.json"); +const curveXChainLiquidityGaugeAbi = require("./abi/curveXChainLiquidityGauge.json"); +const curveStableSwapNGAbi = require("./abi/curveStableSwapNG.json"); +const curveChildLiquidityGaugeFactoryAbi = require("./abi/curveChildLiquidityGaugeFactory.json"); + const log = require("../utils/logger")("test:fixtures-sonic"); const MINTER_ROLE = @@ -126,6 +131,13 @@ const defaultSonicFixture = deployments.createFixture(async () => { await sonicStakingStrategy.connect(strategist).setDefaultValidatorId(18); } + // Curve + const curveAMOProxy = await ethers.getContract("SonicCurveAMOStrategyProxy"); + const curveAMOStrategy = await ethers.getContractAt( + "SonicCurveAMOStrategy", + curveAMOProxy.address + ); + for (const user of [rafael, nick, clement]) { // Mint some Sonic Wrapped S await hhHelpers.setBalance(user.address, oethUnits("100000000")); @@ -135,6 +147,23 @@ const defaultSonicFixture = deployments.createFixture(async () => { await wS.connect(user).approve(oSonicVault.address, oethUnits("5000")); } + const curvePool = await ethers.getContractAt( + curveStableSwapNGAbi, + addresses.sonic.WS_OS.pool + ); + + const curveGauge = await ethers.getContractAt( + curveXChainLiquidityGaugeAbi, + addresses.sonic.WS_OS.gauge + ); + + const curveChildLiquidityGaugeFactory = await ethers.getContractAt( + curveChildLiquidityGaugeFactoryAbi, + addresses.sonic.childLiquidityGaugeFactory + ); + + const crv = await ethers.getContractAt(erc20Abi, addresses.sonic.CRV); + return { // Origin S oSonic, @@ -149,6 +178,13 @@ const defaultSonicFixture = deployments.createFixture(async () => { // Wrapped S wS, + // Curve + curveAMOStrategy, + curvePool, + curveGauge, + curveChildLiquidityGaugeFactory, + crv, + // Signers governor, strategist, diff --git a/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js b/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js index e866275bb3..4a758867f7 100644 --- a/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js +++ b/contracts/test/strategies/base/aerodrome-amo.base.fork-test.js @@ -18,7 +18,7 @@ const baseFixture = createFixtureLoader(defaultBaseFixture); const { setERC20TokenBalance } = require("../../_fund"); const futureEpoch = 1924064072; -describe("ForkTest: Aerodrome AMO Strategy empty pool setup (Base)", async function () { +describe("Base Fork Test: Aerodrome AMO Strategy empty pool setup (Base)", async function () { let fixture, oethbVault, oethb, diff --git a/contracts/test/strategies/base/bridged-woeth-strategy.base.fork-test.js b/contracts/test/strategies/base/bridged-woeth-strategy.base.fork-test.js index 7120b7a009..246d0f09ca 100644 --- a/contracts/test/strategies/base/bridged-woeth-strategy.base.fork-test.js +++ b/contracts/test/strategies/base/bridged-woeth-strategy.base.fork-test.js @@ -8,7 +8,7 @@ const addresses = require("../../../utils/addresses"); const baseFixture = createFixtureLoader(defaultBaseFixture); -describe("ForkTest: Bridged WOETH Strategy", function () { +describe("Base Fork Test: Bridged WOETH Strategy", function () { let fixture; beforeEach(async () => { fixture = await baseFixture(); diff --git a/contracts/test/strategies/base/bridged-woeth-strategy.base.js b/contracts/test/strategies/base/bridged-woeth-strategy.base.js index feae40e1f0..c823191580 100644 --- a/contracts/test/strategies/base/bridged-woeth-strategy.base.js +++ b/contracts/test/strategies/base/bridged-woeth-strategy.base.js @@ -5,7 +5,7 @@ const { oethUnits } = require("../../helpers"); const baseFixture = createFixtureLoader(defaultBaseFixture); -describe("Bridged WOETH Strategy", function () { +describe("Base Fork Test: Bridged WOETH Strategy", function () { let fixture; beforeEach(async () => { fixture = await baseFixture(); diff --git a/contracts/test/strategies/base/curve-amo.base.fork-test.js b/contracts/test/strategies/base/curve-amo.base.fork-test.js index 1987915ece..fda8508f61 100644 --- a/contracts/test/strategies/base/curve-amo.base.fork-test.js +++ b/contracts/test/strategies/base/curve-amo.base.fork-test.js @@ -13,7 +13,7 @@ const { shouldBehaveLikeStrategy } = require("../../behaviour/strategy"); const baseFixture = createFixtureLoader(defaultBaseFixture); -describe("Curve AMO strategy", function () { +describe("Base Fork Test: Curve AMO strategy", function () { let fixture, oethbVault, curveAMOStrategy, diff --git a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js new file mode 100644 index 0000000000..8aefd8932d --- /dev/null +++ b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js @@ -0,0 +1,737 @@ +const { createFixtureLoader } = require("../../_fixture"); +const { defaultSonicFixture } = require("../../_fixture-sonic"); +const { expect } = require("chai"); +const { oethUnits } = require("../../helpers"); +const addresses = require("../../../utils/addresses"); +const { impersonateAndFund } = require("../../../utils/signers"); +const { setERC20TokenBalance } = require("../../_fund"); +const hre = require("hardhat"); +const { advanceTime } = require("../../helpers"); +const { shouldBehaveLikeGovernable } = require("../../behaviour/governable"); +const { shouldBehaveLikeHarvestable } = require("../../behaviour/harvestable"); +const { shouldBehaveLikeStrategy } = require("../../behaviour/strategy"); + +const sonicFixture = createFixtureLoader(defaultSonicFixture); + +describe("Sonic Fork Test: Curve AMO strategy", function () { + let fixture, vault, curveAMOStrategy, os, ws, nick, clement, rafael, timelock; + + let curvePool, + curveGauge, + impersonatedVaultSigner, + impersonatedStrategist, + impersonatedHarvester, + impersonatedCurveGaugeFactory, + impersonatedAMOGovernor, + impersonatedCurveStrategy, + curveChildLiquidityGaugeFactory, + impersonatedTimelock, + crv, + harvester; + + let defaultDepositor; + + const defaultDeposit = oethUnits("5"); + + beforeEach(async () => { + fixture = await sonicFixture(); + vault = fixture.oSonicVault; + curveAMOStrategy = fixture.curveAMOStrategy; + os = fixture.oSonic; + ws = fixture.wS; + nick = fixture.nick; + rafael = fixture.rafael; + clement = fixture.clement; + timelock = fixture.timelock; + curvePool = fixture.curvePool; + curveGauge = fixture.curveGauge; + curveChildLiquidityGaugeFactory = fixture.curveChildLiquidityGaugeFactory; + crv = fixture.crv; + // harvester = fixture.harvester; + + defaultDepositor = rafael; + + impersonatedVaultSigner = await impersonateAndFund(vault.address); + impersonatedStrategist = await impersonateAndFund( + await vault.strategistAddr() + ); + // impersonatedHarvester = await impersonateAndFund(harvester.address); + impersonatedCurveGaugeFactory = await impersonateAndFund( + curveChildLiquidityGaugeFactory.address + ); + impersonatedAMOGovernor = await impersonateAndFund( + await curveAMOStrategy.governor() + ); + impersonatedTimelock = await impersonateAndFund(timelock.address); + impersonatedCurveStrategy = await impersonateAndFund( + curveAMOStrategy.address + ); + + // Set vaultBuffer to 100% + await vault.connect(impersonatedTimelock).setVaultBuffer(oethUnits("1")); + + // await curveAMOStrategy + // .connect(impersonatedAMOGovernor) + // .setHarvesterAddress(harvester.address); + }); + + it("Should have correct parameters after deployment", async () => { + const { curveAMOStrategy } = fixture; + expect(await curveAMOStrategy.platformAddress()).to.equal( + addresses.sonic.WS_OS.pool + ); + expect(await curveAMOStrategy.vaultAddress()).to.equal(vault.address); + expect(await curveAMOStrategy.gauge()).to.equal( + addresses.sonic.WS_OS.gauge + ); + expect(await curveAMOStrategy.curvePool()).to.equal( + addresses.sonic.WS_OS.pool + ); + expect(await curveAMOStrategy.lpToken()).to.equal( + addresses.sonic.WS_OS.pool + ); + expect(await curveAMOStrategy.oeth()).to.equal(os.address); + expect(await curveAMOStrategy.weth()).to.equal(ws.address); + expect(await curveAMOStrategy.governor()).to.equal( + addresses.sonic.timelock + ); + expect(await curveAMOStrategy.rewardTokenAddresses(0)).to.equal( + addresses.sonic.CRV + ); + expect(await curveAMOStrategy.maxSlippage()).to.equal(oethUnits("0.002")); + }); + + describe("Operational functions", () => { + it("Should deposit to strategy", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + expect(await curveAMOStrategy.checkBalance(ws.address)).to.approxEqual( + defaultDeposit.mul(2) + ); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqual(defaultDeposit.mul(2)); + expect(await os.balanceOf(defaultDepositor.address)).to.equal( + defaultDeposit + ); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); + }); + + it("Should deposit all to strategy", async () => { + await balancePool(); + + const amount = defaultDeposit; + const user = defaultDepositor; + + const balance = await ws.balanceOf(user.address); + if (balance < amount) { + await setERC20TokenBalance(user.address, ws, amount + balance, hre); + } + await ws.connect(user).transfer(curveAMOStrategy.address, amount); + + expect(await ws.balanceOf(curveAMOStrategy.address)).to.gt(0); + + await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); + + expect(await curveAMOStrategy.checkBalance(ws.address)).to.approxEqual( + defaultDeposit.mul(2) + ); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqual(defaultDeposit.mul(2)); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); + }); + + it("Should deposit all to strategy with no balance", async () => { + await balancePool(); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); + + await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); + + expect(await curveAMOStrategy.checkBalance(ws.address)).to.eq(0); + expect(await curveGauge.balanceOf(curveAMOStrategy.address)).to.eq(0); + }); + + it("Should withdraw from strategy", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + const impersonatedVaultSigner = await impersonateAndFund(vault.address); + + await curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(vault.address, ws.address, oethUnits("1")); + + expect(await curveAMOStrategy.checkBalance(ws.address)).to.approxEqual( + defaultDeposit.sub(oethUnits("1")).mul(2) + ); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqual(defaultDeposit.sub(oethUnits("1")).mul(2)); + expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); + // Can get a dust amount left + expect(await ws.balanceOf(curveAMOStrategy.address)).to.lte(1); + }); + + it("Should withdraw all from strategy", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + const balanceVault = await ws.balanceOf(vault.address); + + await curveAMOStrategy.connect(impersonatedVaultSigner).withdrawAll(); + + expect( + await curveAMOStrategy.checkBalance(ws.address) + ).to.approxEqualTolerance(0); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(0); + expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + expect(await ws.balanceOf(vault.address)).to.approxEqualTolerance( + balanceVault.add(defaultDeposit) + ); + }); + + it("Should mintAndAddOToken", async () => { + await unbalancePool({ + balancedBefore: true, + wsAmount: defaultDeposit, + }); + + await curveAMOStrategy + .connect(impersonatedStrategist) + .mintAndAddOTokens(defaultDeposit); + + expect( + await curveAMOStrategy.checkBalance(ws.address) + ).to.approxEqualTolerance(defaultDeposit); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit); + expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + }); + + it("Should removeAndBurnOToken", async () => { + await balancePool(); + await mintAndDepositToStrategy({ + userOverride: false, + amount: defaultDeposit.mul(2), + returnTransaction: false, + }); + await unbalancePool({ + balancedBefore: true, + osAmount: defaultDeposit.mul(2), + }); + + await curveAMOStrategy + .connect(impersonatedStrategist) + .removeAndBurnOTokens(defaultDeposit); + + expect( + await curveAMOStrategy.checkBalance(ws.address) + ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); + expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + }); + + it("Should removeOnlyAssets", async () => { + await balancePool(); + await mintAndDepositToStrategy({ + userOverride: false, + amount: defaultDeposit.mul(2), + returnTransaction: false, + }); + await unbalancePool({ + balancedBefore: true, + wsAmount: defaultDeposit.mul(2), + }); + + const vaultETHBalanceBefore = await ws.balanceOf(vault.address); + + await curveAMOStrategy + .connect(impersonatedStrategist) + .removeOnlyAssets(defaultDeposit); + + expect( + await curveAMOStrategy.checkBalance(ws.address) + ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); + expect(await ws.balanceOf(vault.address)).to.approxEqualTolerance( + vaultETHBalanceBefore.add(defaultDeposit) + ); + }); + + it("Should collectRewardTokens", async () => { + await mintAndDepositToStrategy(); + await simulateCRVInflation({ + amount: oethUnits("1000000"), + timejump: 60, + checkpoint: true, + }); + + const balanceCRVHarvesterBefore = await crv.balanceOf(harvester.address); + await curveAMOStrategy + .connect(impersonatedHarvester) + .collectRewardTokens(); + const balanceCRVHarvesterAfter = await crv.balanceOf(harvester.address); + + expect(balanceCRVHarvesterAfter).to.be.gt(balanceCRVHarvesterBefore); + expect(await crv.balanceOf(curveGauge.address)).to.equal(0); + }); + }); + + describe("when pool is heavily unbalanced", () => { + it("Should deposit with OS", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + await unbalancePool({ osAmount: defaultDeposit.mul(1000) }); + + await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); + + expect( + await curveAMOStrategy.checkBalance(ws.address) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); + }); + + it("Should deposit with wS", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + await unbalancePool({ wsAmount: defaultDeposit.mul(1000) }); + + await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); + + expect( + await curveAMOStrategy.checkBalance(ws.address) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); + }); + + it("Should withdraw all with OS", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + await unbalancePool({ osAmount: defaultDeposit.mul(1000) }); + + const checkBalanceAMO = await curveAMOStrategy.checkBalance(ws.address); + const balanceVault = await ws.balanceOf(vault.address); + + await curveAMOStrategy.connect(impersonatedVaultSigner).withdrawAll(); + + expect( + await curveAMOStrategy.checkBalance(ws.address) + ).to.approxEqualTolerance(0); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(0); + expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + expect(await ws.balanceOf(vault.address)).to.approxEqualTolerance( + balanceVault.add(checkBalanceAMO) + ); + }); + + it("Should withdraw all with wS", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + await unbalancePool({ wsAmount: defaultDeposit.mul(1000) }); + const checkBalanceAMO = await curveAMOStrategy.checkBalance(ws.address); + const balanceVault = await ws.balanceOf(vault.address); + + await curveAMOStrategy.connect(impersonatedVaultSigner).withdrawAll(); + + expect( + await curveAMOStrategy.checkBalance(ws.address) + ).to.approxEqualTolerance(0); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(0); + expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + expect(await ws.balanceOf(vault.address)).to.approxEqualTolerance( + balanceVault.add(checkBalanceAMO) + ); + }); + }); + + describe("admin functions", () => { + it("Should set max slippage", async () => { + await curveAMOStrategy + .connect(impersonatedAMOGovernor) + .setMaxSlippage(oethUnits("0.01456")); + + expect(await curveAMOStrategy.maxSlippage()).to.equal( + oethUnits("0.01456") + ); + }); + }); + + describe("Should revert when", () => { + it("Deposit: Must deposit something", async () => { + await expect( + curveAMOStrategy.connect(impersonatedVaultSigner).deposit(ws.address, 0) + ).to.be.revertedWith("Must deposit something"); + }); + it("Deposit: Can only deposit wS", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .deposit(os.address, defaultDeposit) + ).to.be.revertedWith("Can only deposit WETH"); + }); + it("Deposit: Caller is not the Vault", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .deposit(ws.address, defaultDeposit) + ).to.be.revertedWith("Caller is not the Vault"); + }); + it("Deposit: Protocol is insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + // Make protocol insolvent by minting a lot of OETH + // This is a cheat. + // prettier-ignore + await vault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + + await expect( + mintAndDepositToStrategy({ returnTransaction: true }) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Withdraw: Must withdraw something", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(vault.address, ws.address, 0) + ).to.be.revertedWith("Must withdraw something"); + }); + it("Withdraw: Can only withdraw WETH", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(vault.address, os.address, defaultDeposit) + ).to.be.revertedWith("Can only withdraw WETH"); + }); + it("Withdraw: Caller is not the vault", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .withdraw(vault.address, ws.address, defaultDeposit) + ).to.be.revertedWith("Caller is not the Vault"); + }); + it("Withdraw: Amount is greater than balance", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(vault.address, ws.address, oethUnits("1000000")) + ).to.be.revertedWith(""); + }); + it("Withdraw: Protocol is insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); + + // Make protocol insolvent by minting a lot of OETH and send them + // Otherwise they will be burned and the protocol will not be insolvent. + // This is a cheat. + // prettier-ignore + await vault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + await os + .connect(impersonatedCurveStrategy) + .transfer(vault.address, oethUnits("1000000")); + + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(vault.address, ws.address, defaultDeposit) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Mint OToken: Asset overshot peg", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + await unbalancePool({ wsAmount: defaultDeposit }); // +5 WETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .mintAndAddOTokens(defaultDeposit.mul(2)) + ).to.be.revertedWith("Assets overshot peg"); + }); + it("Mint OToken: OTokens balance worse", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + await unbalancePool({ osAmount: defaultDeposit.mul(2) }); // +10 OETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .mintAndAddOTokens(defaultDeposit) + ).to.be.revertedWith("OTokens balance worse"); + }); + it("Mint OToken: Protocol insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + // prettier-ignore + await vault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .mintAndAddOTokens(defaultDeposit) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Burn OToken: Asset balance worse", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + await unbalancePool({ wsAmount: defaultDeposit.mul(2) }); // +10 WETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeAndBurnOTokens(defaultDeposit) + ).to.be.revertedWith("Assets balance worse"); + }); + it("Burn OToken: OTokens overshot peg", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + await unbalancePool({ osAmount: defaultDeposit }); // +5 OETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeAndBurnOTokens(defaultDeposit) + ).to.be.revertedWith("OTokens overshot peg"); + }); + it("Burn OToken: Protocol insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + // prettier-ignore + await vault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeAndBurnOTokens(defaultDeposit) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Remove only assets: Asset overshot peg", async () => { + await balancePool(); + await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); + await unbalancePool({ wsAmount: defaultDeposit.mul(2) }); // +10 WETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeOnlyAssets(defaultDeposit.mul(3)) + ).to.be.revertedWith("Assets overshot peg"); + }); + it("Remove only assets: OTokens balance worse", async () => { + await balancePool(); + await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); + await unbalancePool({ osAmount: defaultDeposit.mul(2) }); // +10 OETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeOnlyAssets(defaultDeposit) + ).to.be.revertedWith("OTokens balance worse"); + }); + it("Remove only assets: Protocol insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); + // prettier-ignore + await vault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeOnlyAssets(defaultDeposit) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Check balance: Unsupported asset", async () => { + await expect( + curveAMOStrategy.checkBalance(os.address) + ).to.be.revertedWith("Unsupported asset"); + }); + it("Max slippage is too high", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedAMOGovernor) + .setMaxSlippage(oethUnits("0.51")) + ).to.be.revertedWith("Slippage must be less than 100%"); + }); + }); + + shouldBehaveLikeGovernable(() => ({ + ...fixture, + anna: rafael, + josh: nick, + matt: clement, + dai: crv, + strategy: curveAMOStrategy, + governor: timelock, + })); + + shouldBehaveLikeHarvestable(() => ({ + ...fixture, + anna: rafael, + strategy: curveAMOStrategy, + harvester, + oeth: os, + })); + + shouldBehaveLikeStrategy(() => ({ + ...fixture, + // Contracts + strategy: curveAMOStrategy, + vault: vault, + assets: [ws], + governor: timelock, + strategist: rafael, + harvester, + crv, + // As we don't have this on base fixture, we use CRV + usdt: crv, + usdc: crv, + dai: crv, + weth: ws, + reth: crv, + stETH: crv, + frxETH: crv, + cvx: crv, + comp: crv, + bal: crv, + // Users + anna: rafael, + matt: clement, + josh: nick, + })); + + const mintAndDepositToStrategy = async ({ + userOverride, + amount, + returnTransaction, + } = {}) => { + const user = userOverride || defaultDepositor; + amount = amount || defaultDeposit; + + const balance = await ws.balanceOf(user.address); + if (balance < amount) { + await setERC20TokenBalance(user.address, ws, amount + balance, hre); + } + await ws.connect(user).approve(vault.address, amount); + await vault.connect(user).mint(ws.address, amount, amount); + + const gov = await vault.governor(); + const tx = await vault + .connect(await impersonateAndFund(gov)) + .depositToStrategy(curveAMOStrategy.address, [ws.address], [amount]); + + if (returnTransaction) { + return tx; + } + + await expect(tx).to.emit(curveAMOStrategy, "Deposit"); + }; + + const balancePool = async () => { + let balances = await curvePool.get_balances(); + const balanceWETH = balances[0]; + const balanceOETH = balances[1]; + + if (balanceWETH > balanceOETH) { + const amount = balanceWETH.sub(balanceOETH); + const balance = ws.balanceOf(nick.address); + if (balance < amount) { + await setERC20TokenBalance(nick.address, ws, amount + balance, hre); + } + await ws.connect(nick).approve(vault.address, amount.mul(101).div(10)); + await vault + .connect(nick) + .mint(ws.address, amount.mul(101).div(10), amount); + await os.connect(nick).approve(curvePool.address, amount); + // prettier-ignore + await curvePool + .connect(nick)["add_liquidity(uint256[],uint256)"]([amount, 0], 0); + } else if (balanceWETH < balanceOETH) { + const amount = balanceOETH.sub(balanceWETH); + const balance = ws.balanceOf(nick.address); + if (balance < amount) { + await setERC20TokenBalance(nick.address, ws, amount + balance, hre); + } + await ws.connect(nick).approve(curvePool.address, amount); + // prettier-ignore + await curvePool + .connect(nick)["add_liquidity(uint256[],uint256)"]([0, amount], 0); + } + + balances = await curvePool.get_balances(); + expect(balances[0]).to.approxEqualTolerance(balances[1]); + }; + + const unbalancePool = async ({ balancedBefore, wsAmount, osAmount } = {}) => { + if (balancedBefore) { + await balancePool(); + } + + if (wsAmount) { + const balance = ws.balanceOf(nick.address); + if (balance < wsAmount) { + await setERC20TokenBalance(nick.address, ws, wsAmount + balance, hre); + } + await ws.connect(nick).approve(curvePool.address, wsAmount); + // prettier-ignore + await curvePool + .connect(nick)["add_liquidity(uint256[],uint256)"]([0, wsAmount], 0); + } else { + const balance = ws.balanceOf(nick.address); + if (balance < osAmount) { + await setERC20TokenBalance(nick.address, ws, osAmount + balance, hre); + } + await ws.connect(nick).approve(vault.address, osAmount); + await vault.connect(nick).mint(ws.address, osAmount, osAmount); + await os.connect(nick).approve(curvePool.address, osAmount); + // prettier-ignore + await curvePool + .connect(nick)["add_liquidity(uint256[],uint256)"]([osAmount, 0], 0); + } + }; + + const simulateCRVInflation = async ({ + amount, + timejump, + checkpoint, + } = {}) => { + await setERC20TokenBalance(curveGauge.address, crv, amount, hre); + await advanceTime(timejump); + if (checkpoint) { + curveGauge + .connect(impersonatedCurveGaugeFactory) + .user_checkpoint(curveAMOStrategy.address); + } + }; +}); diff --git a/contracts/test/strategies/sonicStaking.sonic.fork-test.js b/contracts/test/strategies/sonic/sonicStaking.sonic.fork-test.js similarity index 68% rename from contracts/test/strategies/sonicStaking.sonic.fork-test.js rename to contracts/test/strategies/sonic/sonicStaking.sonic.fork-test.js index ff761cc08c..70f9974439 100644 --- a/contracts/test/strategies/sonicStaking.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/sonicStaking.sonic.fork-test.js @@ -1,10 +1,10 @@ -const { defaultSonicFixture } = require("./../_fixture-sonic"); +const { defaultSonicFixture } = require("../../_fixture-sonic"); const { shouldBehaveLikeASFCStakingStrategy, -} = require("../behaviour/sfcStakingStrategy"); -const addresses = require("../../utils/addresses"); +} = require("../../behaviour/sfcStakingStrategy"); +const addresses = require("../../../utils/addresses"); -describe("Sonic ForkTest: Sonic Staking Strategy", function () { +describe("Sonic Fork Test: Sonic Staking Strategy", function () { this.timeout(0); let fixture; From 3abade887ac508e77572c4d94c97cedc00282b5f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 14 Feb 2025 18:26:08 +1100 Subject: [PATCH 09/80] Fixed OETH Vault unit tests --- contracts/contracts/vault/OETHVaultAdmin.sol | 2 +- contracts/contracts/vault/OETHVaultCore.sol | 2 +- contracts/test/vault/oeth-vault.js | 33 +++++++++----------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/contracts/contracts/vault/OETHVaultAdmin.sol b/contracts/contracts/vault/OETHVaultAdmin.sol index b80c300b36..88c61eb095 100644 --- a/contracts/contracts/vault/OETHVaultAdmin.sol +++ b/contracts/contracts/vault/OETHVaultAdmin.sol @@ -21,7 +21,7 @@ contract OETHVaultAdmin is VaultAdmin { weth = _weth; // prevent implementation contract to be governed - _setGovernor(address(0)); + _setGovernor(address(1)); } /** diff --git a/contracts/contracts/vault/OETHVaultCore.sol b/contracts/contracts/vault/OETHVaultCore.sol index ac6701306b..a03adbfcff 100644 --- a/contracts/contracts/vault/OETHVaultCore.sol +++ b/contracts/contracts/vault/OETHVaultCore.sol @@ -28,7 +28,7 @@ contract OETHVaultCore is VaultCore { weth = _weth; // prevent implementation contract to be governed - _setGovernor(address(0)); + _setGovernor(address(1)); } /** diff --git a/contracts/test/vault/oeth-vault.js b/contracts/test/vault/oeth-vault.js index 35a3eb9913..d8b432a6d4 100644 --- a/contracts/test/vault/oeth-vault.js +++ b/contracts/test/vault/oeth-vault.js @@ -335,15 +335,14 @@ describe("OETH Vault", function () { it("Fail to cacheWETHAssetIndex if WETH is not a supported asset", async () => { const { frxETH, weth } = fixture; - const { deployerAddr } = await hre.getNamedAccounts(); - const sDeployer = hre.ethers.provider.getSigner(deployerAddr); await deployWithConfirmation("MockOETHVault", [weth.address]); const mockVault = await hre.ethers.getContract("MockOETHVault"); await mockVault.supportAsset(frxETH.address); - const tx = mockVault.connect(sDeployer).cacheWETHAssetIndex(); + const mockGovernor = await impersonateAndFund(addresses.dead); + const tx = mockVault.connect(mockGovernor).cacheWETHAssetIndex(); await expect(tx).to.be.revertedWith("Invalid WETH Asset Index"); }); @@ -441,7 +440,10 @@ describe("OETH Vault", function () { it("Should allow strategy to burnForStrategy", async () => { const { oethVault, oeth, weth, governor, daniel } = fixture; - await oethVault.connect(governor).setOusdMetaStrategy(daniel.address); + await oethVault.connect(governor).approveStrategy(daniel.address); + await oethVault + .connect(governor) + .addStrategyToMintWhitelist(daniel.address); // First increase netOusdMintForStrategyThreshold await oethVault @@ -463,26 +465,21 @@ describe("OETH Vault", function () { ); }); - it("Fail when burnForStrategy because Amoount too high", async () => { + it("Fail when burnForStrategy because amount > int256 ", async () => { const { oethVault, governor, daniel } = fixture; - await oethVault.connect(governor).setOusdMetaStrategy(daniel.address); + await oethVault.connect(governor).approveStrategy(daniel.address); + await oethVault + .connect(governor) + .addStrategyToMintWhitelist(daniel.address); + const tx = oethVault .connect(daniel) .burnForStrategy(parseUnits("10", 76)); - await expect(tx).to.be.revertedWith("Amount too high"); - }); - - it("Fail when burnForStrategy because Attempting to burn too much OUSD.", async () => { - const { oethVault, governor, daniel } = fixture; - - await oethVault.connect(governor).setOusdMetaStrategy(daniel.address); - - // Then try to burn more than authorized - const tx = oethVault.connect(daniel).burnForStrategy(oethUnits("0")); - - await expect(tx).to.be.revertedWith("Attempting to burn too much OUSD."); + await expect(tx).to.be.revertedWith( + "SafeCast: value doesn't fit in an int256" + ); }); }); From e6a7c4b39660843b9b61d4b80bbf5c859ad4fb3d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 14 Feb 2025 18:26:55 +1100 Subject: [PATCH 10/80] Changed logging in withConfirmation function --- contracts/utils/deploy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index f24bf2e2c7..9b140262bb 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -138,7 +138,7 @@ const withConfirmation = async ( ? process.env.HOLESKY_PROVIDER_URL : process.env.PROVIDER_URL; if (providerUrl?.includes("rpc.tenderly.co") || (isTest && !isForkTest)) { - console.log("Skipping confirmation on Tenderly or for unit tests"); + log("Skipping confirmation on Tenderly or for unit tests"); // Skip on Tenderly and for unit tests return result; } From 07ee3c8c4dcb143441d021e9c1cbbb8e2e900840 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 14 Feb 2025 22:02:19 +1100 Subject: [PATCH 11/80] FIxed logging in findBalancesSlot --- contracts/test/_fund.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/_fund.js b/contracts/test/_fund.js index e2d5d336cf..0bd6969f1a 100644 --- a/contracts/test/_fund.js +++ b/contracts/test/_fund.js @@ -44,7 +44,7 @@ const balancesContractSlotCache = { const findBalancesSlot = async (tokenAddress) => { tokenAddress = tokenAddress.toLowerCase(); if (balancesContractSlotCache[tokenAddress]) { - console.log( + log( `Found balance slot ${balancesContractSlotCache[tokenAddress]} for ${tokenAddress} in cache` ); return balancesContractSlotCache[tokenAddress]; From f919c9f363e2153d6a804920c6866e23ceaab214 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 14 Feb 2025 22:36:17 +1100 Subject: [PATCH 12/80] Added upgrade of the OETH Vault so it can use multiple AMOs --- .../deploy/mainnet/123_oeth_vault_upgrade.js | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 contracts/deploy/mainnet/123_oeth_vault_upgrade.js diff --git a/contracts/deploy/mainnet/123_oeth_vault_upgrade.js b/contracts/deploy/mainnet/123_oeth_vault_upgrade.js new file mode 100644 index 0000000000..59f9530940 --- /dev/null +++ b/contracts/deploy/mainnet/123_oeth_vault_upgrade.js @@ -0,0 +1,59 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "124_oeth_vault_upgrade", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + // proposalId: "", + }, + async ({ deployWithConfirmation }) => { + // Deployer Actions + // ---------------- + + // 1. Deploy new OETH Vault Core and Admin implementations + const dVaultCore = await deployWithConfirmation("OETHVaultCore", [ + addresses.mainnet.WETH, + ]); + const dVaultAdmin = await deployWithConfirmation("OETHVaultAdmin", [ + addresses.mainnet.WETH, + ]); + + // 2. Connect to the OETH Vault as its governor via the proxy + const cVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cVault = await ethers.getContractAt("IVault", cVaultProxy.address); + + const cCurveAMOStrategyProxy = await ethers.getContract( + "ConvexEthMetaStrategyProxy" + ); + + // Governance Actions + // ---------------- + return { + name: "Upgrade OETH Vault", + actions: [ + // 1. Upgrade the OETH Vault proxy to the new core vault implementation + { + contract: cVaultProxy, + signature: "upgradeTo(address)", + args: [dVaultCore.address], + }, + // 2. Set OETH Vault proxy to the new admin vault implementation + { + contract: cVault, + signature: "setAdminImpl(address)", + args: [dVaultAdmin.address], + }, + // 3. Add the Curve AMO as a whitelisted address + { + contract: cVault, + signature: "addStrategyToMintWhitelist(address)", + args: [cCurveAMOStrategyProxy.address], + }, + ], + }; + } +); From 75949f4738c366f60fb6cdc2029666617bd7abdc Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 14 Feb 2025 23:04:21 +1100 Subject: [PATCH 13/80] Removed CRV and CVX from OUSD Harvester --- .../test/vault/vault.mainnet.fork-test.js | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/contracts/test/vault/vault.mainnet.fork-test.js b/contracts/test/vault/vault.mainnet.fork-test.js index dda24e4b6a..71e9ebf9ef 100644 --- a/contracts/test/vault/vault.mainnet.fork-test.js +++ b/contracts/test/vault/vault.mainnet.fork-test.js @@ -420,26 +420,6 @@ describe("ForkTest: Vault", function () { uniswapV3Path: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9002710c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7", }, - [fixture.cvx.address]: { - allowedSlippageBps: 300, - harvestRewardBps: 100, - swapPlatformAddr: "0xE592427A0AEce92De3Edee1F18E0157C05861564", - doSwapRewardToken: true, - swapPlatform: 1, - liquidationLimit: ousdUnits("2500"), - uniswapV3Path: - "0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b002710c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7", - }, - [fixture.crv.address]: { - allowedSlippageBps: 300, - harvestRewardBps: 200, - swapPlatformAddr: "0xE592427A0AEce92De3Edee1F18E0157C05861564", - doSwapRewardToken: true, - swapPlatform: 1, - liquidationLimit: ousdUnits("4000"), - uniswapV3Path: - "0xd533a949740bb3306d119cc777fa900ba034cd52000bb8c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4dac17f958d2ee523a2206206994597c13d831ec7", - }, }, })); }); From 44f7fef08a1bcf61ff5f68996ff1501a9a0620f5 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 17 Feb 2025 15:23:42 +1100 Subject: [PATCH 14/80] Added Harvester to Sonic --- .../contracts/harvest/OETHHarvesterSimple.sol | 3 + .../contracts/harvest/OSonicHarvester.sol | 11 +++ .../interfaces/sonic/IWrappedSonic.sol | 9 ++ contracts/contracts/proxies/SonicProxies.sol | 2 +- contracts/deploy/sonic/009_curve_amo.js | 45 +++++++++- contracts/deploy/sonic/009_harvester.js | 57 ------------ contracts/test/_fixture-sonic.js | 12 ++- .../sonic/curve-amo.sonic.fork-test.js | 86 ++++++++++++------- 8 files changed, 134 insertions(+), 91 deletions(-) create mode 100644 contracts/contracts/harvest/OSonicHarvester.sol delete mode 100644 contracts/deploy/sonic/009_harvester.js diff --git a/contracts/contracts/harvest/OETHHarvesterSimple.sol b/contracts/contracts/harvest/OETHHarvesterSimple.sol index 6acc3f0944..3b0729e590 100644 --- a/contracts/contracts/harvest/OETHHarvesterSimple.sol +++ b/contracts/contracts/harvest/OETHHarvesterSimple.sol @@ -48,6 +48,9 @@ contract OETHHarvesterSimple is Initializable, Strategizable { //////////////////////////////////////////////////// constructor(address _wrappedNativeToken) { wrappedNativeToken = _wrappedNativeToken; + + // prevent implementation contract to be governed + _setGovernor(address(1)); } /// @notice Initialize the contract diff --git a/contracts/contracts/harvest/OSonicHarvester.sol b/contracts/contracts/harvest/OSonicHarvester.sol new file mode 100644 index 0000000000..f1cf642a34 --- /dev/null +++ b/contracts/contracts/harvest/OSonicHarvester.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { SuperOETHHarvester } from "./SuperOETHHarvester.sol"; + +contract OSonicHarvester is SuperOETHHarvester { + /// @param _wrappedNativeToken Address of the native Wrapped S (wS) token + constructor(address _wrappedNativeToken) + SuperOETHHarvester(_wrappedNativeToken) + {} +} diff --git a/contracts/contracts/interfaces/sonic/IWrappedSonic.sol b/contracts/contracts/interfaces/sonic/IWrappedSonic.sol index 7991187436..fe05cdef6d 100644 --- a/contracts/contracts/interfaces/sonic/IWrappedSonic.sol +++ b/contracts/contracts/interfaces/sonic/IWrappedSonic.sol @@ -2,6 +2,15 @@ pragma solidity ^0.8.0; interface IWrappedSonic { + event Deposit(address indexed account, uint256 value); + event Withdrawal(address indexed account, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + function allowance(address owner, address spender) external view diff --git a/contracts/contracts/proxies/SonicProxies.sol b/contracts/contracts/proxies/SonicProxies.sol index 6ed690d4a6..afce5699c7 100644 --- a/contracts/contracts/proxies/SonicProxies.sol +++ b/contracts/contracts/proxies/SonicProxies.sol @@ -39,7 +39,7 @@ contract SonicStakingStrategyProxy is InitializeGovernedUpgradeabilityProxy { } /** - * @notice OSonicHarvesterProxy delegates calls to a OETHHarvesterSimple implementation + * @notice OSonicHarvesterProxy delegates calls to a OSonicHarvester implementation */ contract OSonicHarvesterProxy is InitializeGovernedUpgradeabilityProxy { diff --git a/contracts/deploy/sonic/009_curve_amo.js b/contracts/deploy/sonic/009_curve_amo.js index 49bbff5908..4b2b777ed9 100644 --- a/contracts/deploy/sonic/009_curve_amo.js +++ b/contracts/deploy/sonic/009_curve_amo.js @@ -11,7 +11,7 @@ module.exports = deployOnSonic( deployName: "009_curve_amo", }, async ({ ethers }) => { - const { deployerAddr } = await getNamedAccounts(); + const { deployerAddr, strategistAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); // Deploy Sonic Curve AMO Strategy proxy @@ -78,6 +78,37 @@ module.exports = deployOnSonic( `Deployed Origin Sonic Vault Admin to ${dOSonicVaultAdmin.address}` ); + // Deploy the Harvester proxy + await deployWithConfirmation("OSonicHarvesterProxy"); + + // Deploy the Harvester implementation + await deployWithConfirmation("OETHHarvesterSimple", [addresses.sonic.wS]); + const dHarvester = await ethers.getContract("OETHHarvesterSimple"); + + const cHarvesterProxy = await ethers.getContract("OSonicHarvesterProxy"); + const cHarvester = await ethers.getContractAt( + "OETHHarvesterSimple", + cHarvesterProxy.address + ); + + const cDripperProxy = await ethers.getContract("OSonicDripperProxy"); + + const initSonicStakingStrategy = cHarvester.interface.encodeFunctionData( + "initialize(address,address,address)", + [addresses.sonic.timelock, strategistAddr, cDripperProxy.address] + ); + + // Initialize the Harvester + // prettier-ignore + await withConfirmation( + cHarvesterProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dHarvester.address, + addresses.sonic.timelock, + initSonicStakingStrategy + ) + ); + return { actions: [ // 1. Upgrade Vault proxy to new VaultCore @@ -104,6 +135,18 @@ module.exports = deployOnSonic( signature: "addStrategyToMintWhitelist(address)", args: [cSonicCurveAMOStrategyProxy.address], }, + // 5. Enable for Curve AMO after it has been deployed + { + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cSonicCurveAMOStrategyProxy.address, true], + }, + // 6. Set the Harvester on the Curve AMO strategy + { + contract: cSonicCurveAMOStrategy, + signature: "setHarvesterAddress(address)", + args: [cHarvesterProxy.address], + }, ], }; } diff --git a/contracts/deploy/sonic/009_harvester.js b/contracts/deploy/sonic/009_harvester.js deleted file mode 100644 index 6b78a4ce55..0000000000 --- a/contracts/deploy/sonic/009_harvester.js +++ /dev/null @@ -1,57 +0,0 @@ -const addresses = require("../../utils/addresses.js"); -const { deployOnSonic } = require("../../utils/deploy-l2.js"); -const { - deployWithConfirmation, - withConfirmation, -} = require("../../utils/deploy.js"); - -module.exports = deployOnSonic( - { - deployName: "009_harvester", - }, - async ({ ethers }) => { - const { deployerAddr, strategistAddr } = await getNamedAccounts(); - - const sDeployer = await ethers.provider.getSigner(deployerAddr); - await deployWithConfirmation("OSonicHarvesterProxy"); - - await deployWithConfirmation("OETHHarvesterSimple", [addresses.sonic.wS]); - const dHarvester = await ethers.getContract("OETHHarvesterSimple"); - - const cHarvesterProxy = await ethers.getContract("OSonicHarvesterProxy"); - const cHarvester = await ethers.getContractAt( - "OETHHarvesterSimple", - cHarvesterProxy.address - ); - - const cDripperProxy = await ethers.getContract("OSonicDripperProxy"); - - // const cStakingStrategyProxy = await ethers.getContract("SonicStakingStrategyProxy"); - - const initSonicStakingStrategy = cHarvester.interface.encodeFunctionData( - "initialize(address,address,address)", - [addresses.sonic.timelock, strategistAddr, cDripperProxy.address] - ); - - // prettier-ignore - await withConfirmation( - cHarvesterProxy - .connect(sDeployer)["initialize(address,address,bytes)"]( - dHarvester.address, - addresses.sonic.timelock, - initSonicStakingStrategy - ) - ); - - return { - actions: [ - // TODO: Enable for Curve AMO after it has been deployed - // { - // contract: cHarvesterProxy, - // signature: "setSupportedStrategy(address,bool)", - // args: [cStakingStrategyProxy.address, true], - // }, - ], - }; - } -); diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 6ffb455558..54d2f70915 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -81,8 +81,15 @@ const defaultSonicFixture = deployments.createFixture(async () => { const sfc = await ethers.getContractAt("ISFC", addresses.sonic.SFC); - let dripper, zapper; + let harvester, dripper, zapper; if (isFork) { + // Harvester + const harvesterProxy = await ethers.getContract("OSonicHarvesterProxy"); + harvester = await ethers.getContractAt( + "OSonicHarvester", + harvesterProxy.address + ); + // Dripper const dripperProxy = await ethers.getContract("OSonicDripperProxy"); dripper = await ethers.getContractAt( @@ -169,8 +176,7 @@ const defaultSonicFixture = deployments.createFixture(async () => { oSonic, oSonicVault, wOSonic, - // harvester, - // dripper, + harvester, sonicStakingStrategy, dripper, zapper, diff --git a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js index 8aefd8932d..2d00ee94bb 100644 --- a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js @@ -47,7 +47,7 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { curveGauge = fixture.curveGauge; curveChildLiquidityGaugeFactory = fixture.curveChildLiquidityGaugeFactory; crv = fixture.crv; - // harvester = fixture.harvester; + harvester = fixture.harvester; defaultDepositor = rafael; @@ -55,7 +55,7 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { impersonatedStrategist = await impersonateAndFund( await vault.strategistAddr() ); - // impersonatedHarvester = await impersonateAndFund(harvester.address); + impersonatedHarvester = await impersonateAndFund(harvester.address); impersonatedCurveGaugeFactory = await impersonateAndFund( curveChildLiquidityGaugeFactory.address ); @@ -69,10 +69,6 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { // Set vaultBuffer to 100% await vault.connect(impersonatedTimelock).setVaultBuffer(oethUnits("1")); - - // await curveAMOStrategy - // .connect(impersonatedAMOGovernor) - // .setHarvesterAddress(harvester.address); }); it("Should have correct parameters after deployment", async () => { @@ -104,14 +100,25 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { describe("Operational functions", () => { it("Should deposit to strategy", async () => { await balancePool(); - await mintAndDepositToStrategy(); - expect(await curveAMOStrategy.checkBalance(ws.address)).to.approxEqual( - defaultDeposit.mul(2) + const checkBalanceBefore = await curveAMOStrategy.checkBalance( + ws.address ); + const gaugeBalanceBefore = await curveGauge.balanceOf( + curveAMOStrategy.address + ); + await mintAndDepositToStrategy(); + expect( - await curveGauge.balanceOf(curveAMOStrategy.address) + (await curveAMOStrategy.checkBalance(ws.address)).sub( + checkBalanceBefore + ) ).to.approxEqual(defaultDeposit.mul(2)); + expect( + (await curveGauge.balanceOf(curveAMOStrategy.address)).sub( + gaugeBalanceBefore + ) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); expect(await os.balanceOf(defaultDepositor.address)).to.equal( defaultDeposit ); @@ -123,6 +130,12 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { const amount = defaultDeposit; const user = defaultDepositor; + const checkBalanceBefore = await curveAMOStrategy.checkBalance( + ws.address + ); + const gaugeBalanceBefore = await curveGauge.balanceOf( + curveAMOStrategy.address + ); const balance = await ws.balanceOf(user.address); if (balance < amount) { @@ -131,15 +144,18 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { await ws.connect(user).transfer(curveAMOStrategy.address, amount); expect(await ws.balanceOf(curveAMOStrategy.address)).to.gt(0); - await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); - expect(await curveAMOStrategy.checkBalance(ws.address)).to.approxEqual( - defaultDeposit.mul(2) - ); expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqual(defaultDeposit.mul(2)); + (await curveAMOStrategy.checkBalance(ws.address)).sub( + checkBalanceBefore + ) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); + expect( + (await curveGauge.balanceOf(curveAMOStrategy.address)).sub( + gaugeBalanceBefore + ) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); }); @@ -159,19 +175,29 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { const impersonatedVaultSigner = await impersonateAndFund(vault.address); + const checkBalanceBefore = await curveAMOStrategy.checkBalance( + ws.address + ); + const gaugeBalanceBefore = await curveGauge.balanceOf( + curveAMOStrategy.address + ); + await curveAMOStrategy .connect(impersonatedVaultSigner) .withdraw(vault.address, ws.address, oethUnits("1")); - expect(await curveAMOStrategy.checkBalance(ws.address)).to.approxEqual( - defaultDeposit.sub(oethUnits("1")).mul(2) - ); expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqual(defaultDeposit.sub(oethUnits("1")).mul(2)); + checkBalanceBefore.sub(await curveAMOStrategy.checkBalance(ws.address)) + ).to.approxEqualTolerance(oethUnits("1").mul(2)); + expect( + gaugeBalanceBefore.sub( + await curveGauge.balanceOf(curveAMOStrategy.address) + ) + ).to.approxEqualTolerance(oethUnits("1").mul(2)); expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); - // Can get a dust amount left - expect(await ws.balanceOf(curveAMOStrategy.address)).to.lte(1); + expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); }); it("Should withdraw all from strategy", async () => { @@ -602,12 +628,14 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { strategy: curveAMOStrategy, harvester, oeth: os, + governor: timelock, })); shouldBehaveLikeStrategy(() => ({ ...fixture, // Contracts strategy: curveAMOStrategy, + curveAMOStrategy, vault: vault, assets: [ws], governor: timelock, @@ -660,11 +688,11 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { const balancePool = async () => { let balances = await curvePool.get_balances(); - const balanceWETH = balances[0]; - const balanceOETH = balances[1]; + const balanceOS = balances[0]; + const balanceWS = balances[1]; - if (balanceWETH > balanceOETH) { - const amount = balanceWETH.sub(balanceOETH); + if (balanceWS > balanceOS) { + const amount = balanceWS.sub(balanceOS); const balance = ws.balanceOf(nick.address); if (balance < amount) { await setERC20TokenBalance(nick.address, ws, amount + balance, hre); @@ -677,8 +705,8 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { // prettier-ignore await curvePool .connect(nick)["add_liquidity(uint256[],uint256)"]([amount, 0], 0); - } else if (balanceWETH < balanceOETH) { - const amount = balanceOETH.sub(balanceWETH); + } else if (balanceWS < balanceOS) { + const amount = balanceOS.sub(balanceWS); const balance = ws.balanceOf(nick.address); if (balance < amount) { await setERC20TokenBalance(nick.address, ws, amount + balance, hre); From a50dcf9c7ef5d66842506d52bb4a4bafc5bf9edd Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 17 Feb 2025 16:54:37 +1100 Subject: [PATCH 15/80] Base Curve AMO strategy withdrawAll with no assets --- .../strategies/BaseCurveAMOStrategy.sol | 2 + .../deploy/base/027_base_curve_amo_upgrade.js | 43 +++++++++++++++++++ contracts/test/behaviour/strategy.js | 43 +++++++++++-------- .../base/curve-amo.base.fork-test.js | 1 + .../sonic/curve-amo.sonic.fork-test.js | 2 +- 5 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 contracts/deploy/base/027_base_curve_amo_upgrade.js diff --git a/contracts/contracts/strategies/BaseCurveAMOStrategy.sol b/contracts/contracts/strategies/BaseCurveAMOStrategy.sol index fd14703f82..ca644209c5 100644 --- a/contracts/contracts/strategies/BaseCurveAMOStrategy.sol +++ b/contracts/contracts/strategies/BaseCurveAMOStrategy.sol @@ -340,6 +340,8 @@ contract BaseCurveAMOStrategy is InitializableAbstractStrategy { */ function withdrawAll() external override onlyVaultOrGovernor nonReentrant { uint256 gaugeTokens = gauge.balanceOf(address(this)); + // Can not withdraw zero LP tokens from the gauge + if (gaugeTokens == 0) return; _lpWithdraw(gaugeTokens); // Withdraws are proportional to assets held by 3Pool diff --git a/contracts/deploy/base/027_base_curve_amo_upgrade.js b/contracts/deploy/base/027_base_curve_amo_upgrade.js new file mode 100644 index 0000000000..cdbac1a60a --- /dev/null +++ b/contracts/deploy/base/027_base_curve_amo_upgrade.js @@ -0,0 +1,43 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const { deployWithConfirmation } = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); + +module.exports = deployOnBase( + { + deployName: "027_base_curve_amo_upgrade", + }, + async ({ ethers }) => { + // Deploy Base Curve AMO proxy + const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + + const cOETHBaseCurveAMOProxy = await ethers.getContract( + "OETHBaseCurveAMOProxy" + ); + + // Deploy Base Curve AMO implementation + const dOETHBaseCurveAMOImpl = await deployWithConfirmation( + "BaseCurveAMOStrategy", + [ + [addresses.base.OETHb_WETH.pool, cOETHbVaultProxy.address], + cOETHbProxy.address, + addresses.base.WETH, + addresses.base.OETHb_WETH.gauge, + addresses.base.childLiquidityGaugeFactory, + 1, // SuperOETH is coin 1 of the Curve WETH/SuperOETH pool + 0, // WETH is coin 0 of the Curve WETH/SuperOETH pool + ] + ); + + return { + actions: [ + // Upgrade the Base Curve AMO Strategy implementation + { + contract: cOETHBaseCurveAMOProxy, + signature: "upgradeTo(address)", + args: [dOETHBaseCurveAMOImpl.address], + }, + ], + }; + } +); diff --git a/contracts/test/behaviour/strategy.js b/contracts/test/behaviour/strategy.js index 38dbc7659b..a8dd60e82f 100644 --- a/contracts/test/behaviour/strategy.js +++ b/contracts/test/behaviour/strategy.js @@ -15,6 +15,7 @@ const { parseUnits } = require("ethers/lib/utils"); * - valueAssets: optional array of assets that work for checkBalance and withdraw. defaults to assets * - vault: Vault or OETHVault contract * - harvester: Harvester or OETHHarvester contract + * - checkWithdrawAmounts: Check the amounts in the withdrawAll events. defaults to true * @example shouldBehaveLikeStrategy(() => ({ ...fixture, @@ -23,6 +24,7 @@ const { parseUnits } = require("ethers/lib/utils"); valueAssets: [fixture.frxETH], harvester: fixture.oethHarvester, vault: fixture.oethVault, + checkWithdrawAmounts: true, })); */ const shouldBehaveLikeStrategy = (context) => { @@ -72,6 +74,11 @@ const shouldBehaveLikeStrategy = (context) => { } }); describe("with no assets in the strategy", () => { + beforeEach(async () => { + const { strategy, governor } = context(); + + await strategy.connect(governor).withdrawAll(); + }); it("Should check asset balances", async () => { const { assets, josh, strategy } = context(); @@ -213,12 +220,7 @@ const shouldBehaveLikeStrategy = (context) => { } }); it("Should be able to call withdraw all by vault", async () => { - const { strategy, vault, curveAMOStrategy } = await context(); - - // If strategy is Curve Base AMO, withdrawAll cannot work if there are no assets in the strategy. - // As it will try to remove 0 LPs from the gauge, which is not permitted by Curve gauge. - if (curveAMOStrategy != undefined && curveAMOStrategy == strategy) - return; + const { strategy, vault } = await context(); const vaultSigner = await impersonateAndFund(vault.address); @@ -227,12 +229,8 @@ const shouldBehaveLikeStrategy = (context) => { await expect(tx).to.not.emit(strategy, "Withdrawal"); }); it("Should be able to call withdraw all by governor", async () => { - const { strategy, governor, curveAMOStrategy } = await context(); + const { strategy, governor } = await context(); - // If strategy is Curve Base AMO, withdrawAll cannot work if there are no assets in the strategy. - // As it will try to remove 0 LPs from the gauge, which is not permitted by Curve gauge. - if (curveAMOStrategy != undefined && curveAMOStrategy == strategy) - return; const tx = await strategy.connect(governor).withdrawAll(); await expect(tx).to.not.emit(strategy, "Withdrawal"); @@ -318,7 +316,7 @@ const shouldBehaveLikeStrategy = (context) => { vault, fraxEthStrategy, sfrxETH, - curveAMOStrategy, + checkWithdrawAmounts, } = await context(); const vaultSigner = await impersonateAndFund(vault.address); @@ -341,12 +339,23 @@ const shouldBehaveLikeStrategy = (context) => { withdrawAmount.mul(3) ); } else if ( - curveAMOStrategy != undefined && - curveAMOStrategy == strategy + checkWithdrawAmounts != undefined && + checkWithdrawAmounts == false ) { - // Didn't managed to get this work with args. - await expect(tx).to.emit(strategy, "Withdrawal"); - await expect(tx).to.emit(asset, "Transfer"); + await expect(tx).to.emit(strategy, "Withdrawal").withNamedArgs({ + _asset: asset.address, + _pToken: platformAddress, + }); + + // the transfer does not have to come from the strategy. It can come directly from the platform + // Need to handle WETH which has different named args in the Transfer event + const erc20Asset = await ethers.getContractAt( + "IERC20", + asset.address + ); + await expect(tx) + .to.emit(erc20Asset, "Transfer") + .withNamedArgs({ from: strategy.address, to: vault.address }); } else { await expect(tx) .to.emit(strategy, "Withdrawal") diff --git a/contracts/test/strategies/base/curve-amo.base.fork-test.js b/contracts/test/strategies/base/curve-amo.base.fork-test.js index 4330174976..1999f95e59 100644 --- a/contracts/test/strategies/base/curve-amo.base.fork-test.js +++ b/contracts/test/strategies/base/curve-amo.base.fork-test.js @@ -699,6 +699,7 @@ describe("Base Fork Test: Curve AMO strategy", function () { ...fixture, // Contracts strategy: curveAMOStrategy, + checkWithdrawAmounts: false, vault: oethbVault, assets: [weth], timelock: timelock, diff --git a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js index 2d00ee94bb..15d57ea487 100644 --- a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js @@ -635,7 +635,7 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { ...fixture, // Contracts strategy: curveAMOStrategy, - curveAMOStrategy, + checkWithdrawAmounts: false, vault: vault, assets: [ws], governor: timelock, From 1089129e7eff625cb372e387e33443c9c7fec12c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 17 Feb 2025 18:41:12 +1100 Subject: [PATCH 16/80] Fixed Sonic unit tests --- contracts/test/_fixture-sonic.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 54d2f70915..2a5b72f5c4 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -128,7 +128,7 @@ const defaultSonicFixture = deployments.createFixture(async () => { const oSonicVaultSigner = await impersonateAndFund(oSonicVault.address); - let validatorRegistrator; + let validatorRegistrator, curveAMOStrategy; if (isFork) { validatorRegistrator = await impersonateAndFund( addresses.sonic.validatorRegistrator @@ -136,14 +136,16 @@ const defaultSonicFixture = deployments.createFixture(async () => { validatorRegistrator.address = addresses.sonic.validatorRegistrator; await sonicStakingStrategy.connect(strategist).setDefaultValidatorId(18); - } - // Curve - const curveAMOProxy = await ethers.getContract("SonicCurveAMOStrategyProxy"); - const curveAMOStrategy = await ethers.getContractAt( - "SonicCurveAMOStrategy", - curveAMOProxy.address - ); + // Curve + const curveAMOProxy = await ethers.getContract( + "SonicCurveAMOStrategyProxy" + ); + curveAMOStrategy = await ethers.getContractAt( + "SonicCurveAMOStrategy", + curveAMOProxy.address + ); + } for (const user of [rafael, nick, clement]) { // Mint some Sonic Wrapped S From 17d6c9ec6510aa80b2216876ed91ed533c827130 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 17 Feb 2025 21:32:20 +1100 Subject: [PATCH 17/80] Fixed Base Curve AMO fork tests --- .../base/curve-amo.base.fork-test.js | 80 +++++++++++++++---- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/contracts/test/strategies/base/curve-amo.base.fork-test.js b/contracts/test/strategies/base/curve-amo.base.fork-test.js index 1999f95e59..ed4a1a74c1 100644 --- a/contracts/test/strategies/base/curve-amo.base.fork-test.js +++ b/contracts/test/strategies/base/curve-amo.base.fork-test.js @@ -1,16 +1,20 @@ +const { expect } = require("chai"); +const { formatUnits } = require("ethers/lib/utils"); +const hre = require("hardhat"); + const { createFixtureLoader } = require("../../_fixture"); const { defaultBaseFixture } = require("../../_fixture-base"); -const { expect } = require("chai"); const { oethUnits } = require("../../helpers"); const addresses = require("../../../utils/addresses"); const { impersonateAndFund } = require("../../../utils/signers"); const { setERC20TokenBalance } = require("../../_fund"); -const hre = require("hardhat"); const { advanceTime } = require("../../helpers"); const { shouldBehaveLikeGovernable } = require("../../behaviour/governable"); const { shouldBehaveLikeHarvestable } = require("../../behaviour/harvestable"); const { shouldBehaveLikeStrategy } = require("../../behaviour/strategy"); +const log = require("../../../utils/logger")("test:fork:sonic:curve-amo"); + const baseFixture = createFixtureLoader(defaultBaseFixture); describe("Base Fork Test: Curve AMO strategy", function () { @@ -389,36 +393,65 @@ describe("Base Fork Test: Curve AMO strategy", function () { }); it("Should deposit when pool is heavily unbalanced with OETH", async () => { - await balancePool(); - await mintAndDepositToStrategy(); + await unbalancePool({ oethbAmount: defaultDeposit.mul(20) }); - await unbalancePool({ oethbAmount: defaultDeposit.mul(1000) }); + const checkBalanceBefore = await curveAMOStrategy.checkBalance( + weth.address + ); + log(`AMO checkBalance before deposit ${formatUnits(checkBalanceBefore)}`); + const gaugeTokensBefore = await curveGauge.balanceOf( + curveAMOStrategy.address + ); - await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); + await mintAndDepositToStrategy(); + + const checkBalanceAfter = await curveAMOStrategy.checkBalance( + weth.address + ); + log(`AMO checkBalance after deposit ${formatUnits(checkBalanceAfter)}`); + log( + `AMO checkBalance diff ${formatUnits( + checkBalanceAfter.sub(checkBalanceBefore) + )}` + ); expect( await curveAMOStrategy.checkBalance(weth.address) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); + ).to.approxEqualTolerance(defaultDeposit.mul(2).add(checkBalanceBefore)); expect( await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); + ).to.approxEqualTolerance(defaultDeposit.mul(2).add(gaugeTokensBefore)); expect(await weth.balanceOf(curveAMOStrategy.address)).to.equal(0); }); it("Should deposit when pool is heavily unbalanced with WETH", async () => { - await balancePool(); - await mintAndDepositToStrategy(); + await unbalancePool({ wethbAmount: defaultDeposit.mul(20) }); - await unbalancePool({ wethbAmount: defaultDeposit.mul(1000) }); + const checkBalanceBefore = await curveAMOStrategy.checkBalance( + weth.address + ); + const gaugeTokensBefore = await curveGauge.balanceOf( + curveAMOStrategy.address + ); - await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); + await mintAndDepositToStrategy(); + + const checkBalanceAfter = await curveAMOStrategy.checkBalance( + weth.address + ); + log(`AMO checkBalance after deposit ${formatUnits(checkBalanceAfter)}`); + log( + `AMO checkBalance diff ${formatUnits( + checkBalanceAfter.sub(checkBalanceBefore) + )}` + ); expect( await curveAMOStrategy.checkBalance(weth.address) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); + ).to.approxEqualTolerance(defaultDeposit.mul(3).add(checkBalanceBefore)); expect( await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); + ).to.approxEqualTolerance(defaultDeposit.mul(3).add(gaugeTokensBefore)); expect(await weth.balanceOf(curveAMOStrategy.address)).to.equal(0); }); @@ -739,6 +772,7 @@ describe("Base Fork Test: Curve AMO strategy", function () { await oethbVault.connect(user).mint(weth.address, amount, amount); const gov = await oethbVault.governor(); + log(`Depositing ${formatUnits(amount)} WETH to AMO strategy`); const tx = await oethbVault .connect(await impersonateAndFund(gov)) .depositToStrategy(curveAMOStrategy.address, [weth.address], [amount]); @@ -784,6 +818,9 @@ describe("Base Fork Test: Curve AMO strategy", function () { } balances = await curvePool.get_balances(); + log(`Balanced Curve pool`); + log(`WETH balance: ${formatUnits(balances[0])}`); + log(`OETH balance: ${formatUnits(balances[1])}`); expect(balances[0]).to.approxEqualTolerance(balances[1]); }; @@ -807,6 +844,11 @@ describe("Base Fork Test: Curve AMO strategy", function () { ); } await weth.connect(nick).approve(curvePool.address, wethbAmount); + log( + `Adding ${formatUnits( + wethbAmount + )} WETH to Curve pool to make it unbalanced` + ); // prettier-ignore await curvePool .connect(nick)["add_liquidity(uint256[],uint256)"]([wethbAmount, 0], 0); @@ -825,10 +867,20 @@ describe("Base Fork Test: Curve AMO strategy", function () { .connect(nick) .mint(weth.address, oethbAmount, oethbAmount); await oethb.connect(nick).approve(curvePool.address, oethbAmount); + log( + `Adding ${formatUnits( + oethbAmount + )} OETH to Curve pool to make it unbalanced` + ); // prettier-ignore await curvePool .connect(nick)["add_liquidity(uint256[],uint256)"]([0, oethbAmount], 0); } + + const balances = await curvePool.get_balances(); + log(`Curve pool balances:`); + log(`WETH: ${formatUnits(balances[0])}`); + log(`OETH: ${formatUnits(balances[1])}`); }; const simulateCRVInflation = async ({ From 79874b8772a28e2ddff3ac724df0aef4e8ebcefc Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 Feb 2025 00:07:03 +1100 Subject: [PATCH 18/80] Changed Curve AMO strategy to BUSL-1.1 license --- contracts/contracts/strategies/BaseCurveAMOStrategy.sol | 2 +- contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/strategies/BaseCurveAMOStrategy.sol b/contracts/contracts/strategies/BaseCurveAMOStrategy.sol index ca644209c5..0b473c93db 100644 --- a/contracts/contracts/strategies/BaseCurveAMOStrategy.sol +++ b/contracts/contracts/strategies/BaseCurveAMOStrategy.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; /** diff --git a/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol index 60a8f93e31..972a4714a5 100644 --- a/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; import { BaseCurveAMOStrategy } from "../BaseCurveAMOStrategy.sol"; From affda22ac2d17f320d42fa41ce83d343ffbf2b31 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 Feb 2025 12:50:30 +1100 Subject: [PATCH 19/80] Upgrade Base Vault with Curve AMO strategy --- .../deploy/base/027_base_curve_amo_upgrade.js | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/contracts/deploy/base/027_base_curve_amo_upgrade.js b/contracts/deploy/base/027_base_curve_amo_upgrade.js index cdbac1a60a..fa838d7642 100644 --- a/contracts/deploy/base/027_base_curve_amo_upgrade.js +++ b/contracts/deploy/base/027_base_curve_amo_upgrade.js @@ -7,10 +7,13 @@ module.exports = deployOnBase( deployName: "027_base_curve_amo_upgrade", }, async ({ ethers }) => { - // Deploy Base Curve AMO proxy + // Get the SuperOETH, Vault and Curve AMO contracts const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); - + const cOETHbVault = await ethers.getContractAt( + "IVault", + cOETHbVaultProxy.address + ); const cOETHBaseCurveAMOProxy = await ethers.getContract( "OETHBaseCurveAMOProxy" ); @@ -29,14 +32,37 @@ module.exports = deployOnBase( ] ); + // Deploy new Vault Core implementation + const dOETHbVaultCore = await deployWithConfirmation("OETHBaseVaultCore", [ + addresses.base.WETH, + ]); + + // Deploy new Vault Admin implementation + const dOETHbVaultAdmin = await deployWithConfirmation( + "OETHBaseVaultAdmin", + [addresses.base.WETH] + ); + return { actions: [ - // Upgrade the Base Curve AMO Strategy implementation + // 1. Upgrade the Base Curve AMO Strategy implementation { contract: cOETHBaseCurveAMOProxy, signature: "upgradeTo(address)", args: [dOETHBaseCurveAMOImpl.address], }, + // 2. Upgrade VaultCore + { + contract: cOETHbVaultProxy, + signature: "upgradeTo(address)", + args: [dOETHbVaultCore.address], + }, + // 3. Upgrade VaultAdmin + { + contract: cOETHbVault, + signature: "setAdminImpl(address)", + args: [dOETHbVaultAdmin.address], + }, ], }; } From 214bd77f3d5537b7beb10cdfa3a47383bf38c076 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 Feb 2025 15:53:34 +1100 Subject: [PATCH 20/80] Increase number for Mainnet deploy script --- .../{123_oeth_vault_upgrade.js => 124_oeth_vault_upgrade.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/deploy/mainnet/{123_oeth_vault_upgrade.js => 124_oeth_vault_upgrade.js} (100%) diff --git a/contracts/deploy/mainnet/123_oeth_vault_upgrade.js b/contracts/deploy/mainnet/124_oeth_vault_upgrade.js similarity index 100% rename from contracts/deploy/mainnet/123_oeth_vault_upgrade.js rename to contracts/deploy/mainnet/124_oeth_vault_upgrade.js From d349629ee2a873bda0ec9620d0598dd1d9b62b0a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 Feb 2025 15:54:32 +1100 Subject: [PATCH 21/80] Updated Sonic contract diagrams --- contracts/docs/plantuml/sonicContracts.png | Bin 50708 -> 43673 bytes contracts/docs/plantuml/sonicContracts.puml | 22 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/docs/plantuml/sonicContracts.png b/contracts/docs/plantuml/sonicContracts.png index 913c2b9dcaf3d75d37a94fd94a898b2af79992d1..c5f9892b97d6b02d0944c881937b7213deb97f01 100644 GIT binary patch literal 43673 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 Vz6rHP<+8-zhYxBU$W%FT<)3n++yDRo literal 50708 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 aIrYMMYx(b5y8yW0DFqo-=>o|+&;AGKr*;DX diff --git a/contracts/docs/plantuml/sonicContracts.puml b/contracts/docs/plantuml/sonicContracts.puml index 2a54a3769b..01e5a4f430 100644 --- a/contracts/docs/plantuml/sonicContracts.puml +++ b/contracts/docs/plantuml/sonicContracts.puml @@ -7,13 +7,13 @@ !$changedColor = Orange !$thirdPartyColor = WhiteSmoke -legend -blue - Origin -' green - new -' orange - changed -yellow - phase2 -white - 3rd Party -end legend +' legend +' blue - Origin +' ' green - new +' ' orange - changed +' yellow - phase2 +' white - 3rd Party +' end legend title "Sonic Contract Dependencies" @@ -51,13 +51,13 @@ object "Special Fee Contract" as sfc <><> { asset: S } -object "AMOStrategy" as amoStrat <><> #$phase2 { +object "Curve AMO Strategy" as amoStrat <><> #$originColor { asset: wS - reward: ? + reward: CRV } -object "OSonicHarvester" as harv <><> #$phase2 { - rewards: ? +object "OSonicHarvester" as harv <><> #$originColor { + rewards: CRV } ' Oracle From ecdedc8bccb4f8cb7a0f81e8b575631b7ce2e484 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 Feb 2025 15:55:50 +1100 Subject: [PATCH 22/80] Generated latest Dripper and Harvester diagrams --- contracts/contracts/harvest/README.md | 52 +++- contracts/docs/DripperHierarchy.svg | 136 ++-------- contracts/docs/HarvesterHierarchy.svg | 248 ++---------------- contracts/docs/OETHBaseHarvesterHierarchy.svg | 33 +++ contracts/docs/OETHBaseHarvesterSquashed.svg | 63 +++++ contracts/docs/OETHBaseHarvesterStorage.svg | 29 ++ contracts/docs/OETHDripperHierarchy.svg | 152 ----------- .../docs/OETHFixedRateDripperHierarchy.svg | 59 +++++ ...d.svg => OETHFixedRateDripperSquashed.svg} | 92 +++---- ...ge.svg => OETHFixedRateDripperStorage.svg} | 28 +- contracts/docs/OETHHarvesterHierarchy.svg | 237 ----------------- .../docs/OETHHarvesterSimpleHierarchy.svg | 60 +++++ .../docs/OETHHarvesterSimpleSquashed.svg | 72 +++++ contracts/docs/OETHHarvesterSimpleStorage.svg | 59 +++++ contracts/docs/OETHHarvesterSquashed.svg | 80 ------ contracts/docs/OETHHarvesterStorage.svg | 109 -------- contracts/docs/OSonicHarvesterHierarchy.svg | 86 ++++++ contracts/docs/OSonicHarvesterSquashed.svg | 72 +++++ contracts/docs/OSonicHarvesterStorage.svg | 59 +++++ contracts/docs/generate.sh | 24 +- 20 files changed, 756 insertions(+), 994 deletions(-) create mode 100644 contracts/docs/OETHBaseHarvesterHierarchy.svg create mode 100644 contracts/docs/OETHBaseHarvesterSquashed.svg create mode 100644 contracts/docs/OETHBaseHarvesterStorage.svg delete mode 100644 contracts/docs/OETHDripperHierarchy.svg create mode 100644 contracts/docs/OETHFixedRateDripperHierarchy.svg rename contracts/docs/{OETHDripperSquashed.svg => OETHFixedRateDripperSquashed.svg} (72%) rename contracts/docs/{OETHDripperStorage.svg => OETHFixedRateDripperStorage.svg} (74%) delete mode 100644 contracts/docs/OETHHarvesterHierarchy.svg create mode 100644 contracts/docs/OETHHarvesterSimpleHierarchy.svg create mode 100644 contracts/docs/OETHHarvesterSimpleSquashed.svg create mode 100644 contracts/docs/OETHHarvesterSimpleStorage.svg delete mode 100644 contracts/docs/OETHHarvesterSquashed.svg delete mode 100644 contracts/docs/OETHHarvesterStorage.svg create mode 100644 contracts/docs/OSonicHarvesterHierarchy.svg create mode 100644 contracts/docs/OSonicHarvesterSquashed.svg create mode 100644 contracts/docs/OSonicHarvesterStorage.svg diff --git a/contracts/contracts/harvest/README.md b/contracts/contracts/harvest/README.md index dbc3be1d35..82389cebb2 100644 --- a/contracts/contracts/harvest/README.md +++ b/contracts/contracts/harvest/README.md @@ -2,6 +2,8 @@ ## Dripper +Used by OUSD on Mainnet. + ### Hierarchy ![Dripper Hierarchy](../../docs/DripperHierarchy.svg) @@ -14,22 +16,26 @@ ![Dripper Storage](../../docs/DripperStorage.svg) -## OETH Dripper +## Fixed Rate Dripper + +Used on Mainnet for OETH, Base and Sonic. ### Hierarchy -![OETH Dripper Hierarchy](../../docs/OETHDripperHierarchy.svg) +![Fixed Rate Dripper Hierarchy](../../docs/OETHFixedRateDripperHierarchy.svg) ### Squashed -![OETH Dripper Squashed](../../docs/OETHDripperSquashed.svg) +![Fixed Rate Dripper Squashed](../../docs/OETHFixedRateDripperSquashed.svg) ### Storage -![OETH Dripper Storage](../../docs/OETHDripperStorage.svg) +![Fixed Rate Dripper Storage](../../docs/OETHFixedRateDripperStorage.svg) ## Harvester +Used on Mainnet for OUSD. + ### Hierarchy ![Harvester Hierarchy](../../docs/HarvesterHierarchy.svg) @@ -42,16 +48,46 @@ ![Harvester Storage](../../docs/HarvesterStorage.svg) -## OETH Harvester +## OETH Simple Harvester + +Used on Mainnet for OETH. + +### Hierarchy + +![OETH Simple Harvester Hierarchy](../../docs/OETHHarvesterSimpleHierarchy.svg) + +### Squashed + +![OETH Simple Harvester Squashed](../../docs/OETHHarvesterSimpleSquashed.svg) + +### Storage + +![OETH Simple Harvester Storage](../../docs/OETHHarvesterSimpleStorage.svg) + +## Base Harvester + +### Hierarchy + +![OETH Base Harvester Hierarchy](../../docs/OETHBaseHarvesterHierarchy.svg) + +### Squashed + +![OETH Base Harvester Squashed](../../docs/OETHBaseHarvesterSquashed.svg) + +### Storage + +![OETH Base Harvester Storage](../../docs/OETHBaseHarvesterStorage.svg) + +## Sonic Harvester ### Hierarchy -![OETH Harvester Hierarchy](../../docs/OETHHarvesterHierarchy.svg) +![Sonic Harvester Hierarchy](../../docs/OSonicHarvesterHierarchy.svg) ### Squashed -![OETH Harvester Squashed](../../docs/OETHHarvesterSquashed.svg) +![Sonic Harvester Squashed](../../docs/OSonicHarvesterSquashed.svg) ### Storage -![OETH Harvester Storage](../../docs/OETHHarvesterStorage.svg) +![Sonic Harvester Storage](../../docs/OSonicHarvesterStorage.svg) diff --git a/contracts/docs/DripperHierarchy.svg b/contracts/docs/DripperHierarchy.svg index 0f27419ec5..98caed5df1 100644 --- a/contracts/docs/DripperHierarchy.svg +++ b/contracts/docs/DripperHierarchy.svg @@ -4,136 +4,30 @@ - - + + UmlClassDiagram - - + + -20 - -Governable -../contracts/governance/Governable.sol +21 + +Governable +../contracts/governance/Governable.sol - + -30 +31 Dripper ../contracts/harvest/Dripper.sol - + -30->20 - - - - - -61 - -<<Interface>> -IVault -../contracts/interfaces/IVault.sol - - - -30->61 - - - - - -710 - -<<Interface>> -IERC20 -../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol - - - -30->710 - - - - - -239 - -VaultStorage -../contracts/vault/VaultStorage.sol - - - -61->239 - - - - - -215 - -OUSD -../contracts/token/OUSD.sol - - - -215->20 - - - - - -224 - -<<Abstract>> -Initializable -../contracts/utils/Initializable.sol - - - -215->224 - - - - - -227 - -<<Abstract>> -InitializableERC20Detailed -../contracts/utils/InitializableERC20Detailed.sol - - - -215->227 - - - - - -227->710 - - - - - -239->20 - - - - - -239->215 - - - - - -239->224 - - +31->21 + + diff --git a/contracts/docs/HarvesterHierarchy.svg b/contracts/docs/HarvesterHierarchy.svg index c4334d5fd1..1791e1aa00 100644 --- a/contracts/docs/HarvesterHierarchy.svg +++ b/contracts/docs/HarvesterHierarchy.svg @@ -4,234 +4,44 @@ - - + + UmlClassDiagram - - + + -20 - -Governable -../contracts/governance/Governable.sol +21 + +Governable +../contracts/governance/Governable.sol - + -26 - -<<Abstract>> -AbstractHarvester -../contracts/harvest/AbstractHarvester.sol +27 + +<<Abstract>> +AbstractHarvester +../contracts/harvest/AbstractHarvester.sol - + -26->20 - - +27->21 + + - - -51 - -<<Interface>> -IOracle -../contracts/interfaces/IOracle.sol - - - -26->51 - - - - - -57 - -<<Interface>> -IStrategy -../contracts/interfaces/IStrategy.sol - - - -26->57 - - - - - -61 - -<<Interface>> -IVault -../contracts/interfaces/IVault.sol - - - -26->61 - - - - - -249 - -<<Interface>> -IBalancerVault -../contracts/interfaces/balancer/IBalancerVault.sol - - - -26->249 - - - - - -280 - -<<Interface>> -IUniswapV2Router -../contracts/interfaces/uniswap/IUniswapV2Router02.sol - - - -26->280 - - - - - -281 - -<<Interface>> -IUniswapV3Router -../contracts/interfaces/uniswap/IUniswapV3Router.sol - - - -26->281 - - - - - -199 - -<<Interface>> -ICurvePool -../contracts/strategies/ICurvePool.sol - - - -26->199 - - - - - -710 - -<<Interface>> -IERC20 -../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol - - - -26->710 - - - - + -32 - -Harvester -../contracts/harvest/Harvester.sol - - - -32->26 - - - - - -239 - -VaultStorage -../contracts/vault/VaultStorage.sol - - - -61->239 - - - - - -215 - -OUSD -../contracts/token/OUSD.sol - - - -215->20 - - - - - -224 - -<<Abstract>> -Initializable -../contracts/utils/Initializable.sol +34 + +Harvester +../contracts/harvest/Harvester.sol - - -215->224 - - - - - -227 - -<<Abstract>> -InitializableERC20Detailed -../contracts/utils/InitializableERC20Detailed.sol - - - -215->227 - - - - - -227->710 - - - - - -239->20 - - - - - -239->215 - - - - - -239->224 - - + + +34->27 + + diff --git a/contracts/docs/OETHBaseHarvesterHierarchy.svg b/contracts/docs/OETHBaseHarvesterHierarchy.svg new file mode 100644 index 0000000000..83e0824f5f --- /dev/null +++ b/contracts/docs/OETHBaseHarvesterHierarchy.svg @@ -0,0 +1,33 @@ + + + + + + +UmlClassDiagram + + + +21 + +Governable +../contracts/governance/Governable.sol + + + +35 + +OETHBaseHarvester +../contracts/harvest/OETHBaseHarvester.sol + + + +35->21 + + + + + diff --git a/contracts/docs/OETHBaseHarvesterSquashed.svg b/contracts/docs/OETHBaseHarvesterSquashed.svg new file mode 100644 index 0000000000..cf200cb47c --- /dev/null +++ b/contracts/docs/OETHBaseHarvesterSquashed.svg @@ -0,0 +1,63 @@ + + + + + + +UmlClassDiagram + + + +35 + +OETHBaseHarvester +../contracts/harvest/OETHBaseHarvester.sol + +Private: +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   vault: IVault <<OETHBaseHarvester>> +   amoStrategy: IStrategy <<OETHBaseHarvester>> +   aero: IERC20 <<OETHBaseHarvester>> +   weth: IERC20 <<OETHBaseHarvester>> +   swapRouter: ISwapRouter <<OETHBaseHarvester>> +   operatorAddr: address <<OETHBaseHarvester>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _doSwap(aeroToSwap: uint256, minWETHExpected: uint256) <<OETHBaseHarvester>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setOperatorAddr(_operatorAddr: address) <<onlyGovernor>> <<OETHBaseHarvester>> +    harvest() <<onlyGovernorOrStrategistOrOperator>> <<OETHBaseHarvester>> +    harvestAndSwap(aeroToSwap: uint256, minWETHExpected: uint256, feeBps: uint256, sendYieldToDripper: bool) <<onlyGovernorOrStrategist>> <<OETHBaseHarvester>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<OETHBaseHarvester>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> RewardTokenSwapped(rewardToken: address, swappedInto: address, swapPlatform: uint8, amountIn: uint256, amountOut: uint256) <<OETHBaseHarvester>> +    <<event>> OperatorChanged(oldOperator: address, newOperator: address) <<OETHBaseHarvester>> +    <<event>> YieldSent(recipient: address, yield: uint256, fee: uint256) <<OETHBaseHarvester>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyGovernorOrStrategist() <<OETHBaseHarvester>> +    <<modifier>> onlyGovernorOrStrategistOrOperator() <<OETHBaseHarvester>> +    constructor() <<Governable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    constructor(_vault: address, _amoStrategy: address, _aero: address, _weth: address, _swapRouter: address) <<OETHBaseHarvester>> + + + diff --git a/contracts/docs/OETHBaseHarvesterStorage.svg b/contracts/docs/OETHBaseHarvesterStorage.svg new file mode 100644 index 0000000000..efc29089ec --- /dev/null +++ b/contracts/docs/OETHBaseHarvesterStorage.svg @@ -0,0 +1,29 @@ + + + + + + +StorageDiagram + + + +1 + +OETHBaseHarvester <<Contract>> + +slot + +0 + +type: <inherited contract>.variable (bytes) + +unallocated (12) + +address: operatorAddr (20) + + + diff --git a/contracts/docs/OETHDripperHierarchy.svg b/contracts/docs/OETHDripperHierarchy.svg deleted file mode 100644 index 9641ea80ca..0000000000 --- a/contracts/docs/OETHDripperHierarchy.svg +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - -UmlClassDiagram - - - -20 - -Governable -../contracts/governance/Governable.sol - - - -30 - -Dripper -../contracts/harvest/Dripper.sol - - - -30->20 - - - - - -61 - -<<Interface>> -IVault -../contracts/interfaces/IVault.sol - - - -30->61 - - - - - -710 - -<<Interface>> -IERC20 -../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol - - - -30->710 - - - - - -33 - -OETHDripper -../contracts/harvest/OETHDripper.sol - - - -33->30 - - - - - -239 - -VaultStorage -../contracts/vault/VaultStorage.sol - - - -61->239 - - - - - -215 - -OUSD -../contracts/token/OUSD.sol - - - -215->20 - - - - - -224 - -<<Abstract>> -Initializable -../contracts/utils/Initializable.sol - - - -215->224 - - - - - -227 - -<<Abstract>> -InitializableERC20Detailed -../contracts/utils/InitializableERC20Detailed.sol - - - -215->227 - - - - - -227->710 - - - - - -239->20 - - - - - -239->215 - - - - - -239->224 - - - - - diff --git a/contracts/docs/OETHFixedRateDripperHierarchy.svg b/contracts/docs/OETHFixedRateDripperHierarchy.svg new file mode 100644 index 0000000000..10197dd15b --- /dev/null +++ b/contracts/docs/OETHFixedRateDripperHierarchy.svg @@ -0,0 +1,59 @@ + + + + + + +UmlClassDiagram + + + +21 + +Governable +../contracts/governance/Governable.sol + + + +31 + +Dripper +../contracts/harvest/Dripper.sol + + + +31->21 + + + + + +33 + +FixedRateDripper +../contracts/harvest/FixedRateDripper.sol + + + +33->31 + + + + + +37 + +OETHFixedRateDripper +../contracts/harvest/OETHFixedRateDripper.sol + + + +37->33 + + + + + diff --git a/contracts/docs/OETHDripperSquashed.svg b/contracts/docs/OETHFixedRateDripperSquashed.svg similarity index 72% rename from contracts/docs/OETHDripperSquashed.svg rename to contracts/docs/OETHFixedRateDripperSquashed.svg index 7c76032208..f27d2a587b 100644 --- a/contracts/docs/OETHDripperSquashed.svg +++ b/contracts/docs/OETHFixedRateDripperSquashed.svg @@ -4,55 +4,59 @@ - - + + UmlClassDiagram - - + + -33 - -OETHDripper -../contracts/harvest/OETHDripper.sol - -Private: -   governorPosition: bytes32 <<Governable>> -   pendingGovernorPosition: bytes32 <<Governable>> -   reentryStatusPosition: bytes32 <<Governable>> -Public: -   _NOT_ENTERED: uint256 <<Governable>> -   _ENTERED: uint256 <<Governable>> -   vault: address <<Dripper>> -   token: address <<Dripper>> -   dripDuration: uint256 <<Dripper>> -   drip: Drip <<Dripper>> - -Internal: -    _governor(): (governorOut: address) <<Governable>> -    _pendingGovernor(): (pendingGovernor: address) <<Governable>> -    _setGovernor(newGovernor: address) <<Governable>> -    _setPendingGovernor(newGovernor: address) <<Governable>> -    _changeGovernor(_newGovernor: address) <<Governable>> -    _availableFunds(_balance: uint256, _drip: Drip): uint256 <<Dripper>> -    _collect() <<Dripper>> -External: -    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> -    claimGovernance() <<Governable>> -    availableFunds(): uint256 <<Dripper>> -    collect() <<Dripper>> -    collectAndRebase() <<Dripper>> -    setDripDuration(_durationSeconds: uint256) <<onlyGovernor>> <<Dripper>> -    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<Dripper>> -Public: -    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> -    <<modifier>> onlyGovernor() <<Governable>> -    <<modifier>> nonReentrant() <<Governable>> +37 + +OETHFixedRateDripper +../contracts/harvest/OETHFixedRateDripper.sol + +Private: +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   vault: address <<Dripper>> +   token: address <<Dripper>> +   dripDuration: uint256 <<Dripper>> +   drip: Drip <<Dripper>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _availableFunds(_balance: uint256, _drip: Drip): uint256 <<Dripper>> +    _collect() <<FixedRateDripper>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    availableFunds(): uint256 <<Dripper>> +    collect() <<Dripper>> +    collectAndRebase() <<Dripper>> +    setDripDuration(uint256) <<FixedRateDripper>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<Dripper>> +    transferAllToken(_asset: address, _receiver: address) <<onlyGovernor>> <<Dripper>> +    setDripRate(_perSecond: uint192) <<onlyGovernorOrStrategist>> <<FixedRateDripper>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> DripRateUpdated(oldDripRate: uint192, newDripRate: uint192) <<FixedRateDripper>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyGovernorOrStrategist() <<FixedRateDripper>>    constructor() <<Governable>>    governor(): address <<Governable>>    isGovernor(): bool <<Governable>> -    constructor(_vault: address, _token: address) <<OETHDripper>> +    constructor(_vault: address, _token: address) <<OETHFixedRateDripper>> diff --git a/contracts/docs/OETHDripperStorage.svg b/contracts/docs/OETHFixedRateDripperStorage.svg similarity index 74% rename from contracts/docs/OETHDripperStorage.svg rename to contracts/docs/OETHFixedRateDripperStorage.svg index e2da1f7190..867ab42847 100644 --- a/contracts/docs/OETHDripperStorage.svg +++ b/contracts/docs/OETHFixedRateDripperStorage.svg @@ -4,16 +4,16 @@ - + StorageDiagram - + 2 -OETHDripper <<Contract>> +OETHFixedRateDripper <<Contract>> slot @@ -30,24 +30,24 @@ 1 - -Drip <<Struct>> - + +Drip <<Struct>> + slot 1 -type: variable (bytes) - -uint192: perSecond (24) - -uint64: lastCollect (8) +type: variable (bytes) + +uint192: perSecond (24) + +uint64: lastCollect (8) 2:4->1 - - + + diff --git a/contracts/docs/OETHHarvesterHierarchy.svg b/contracts/docs/OETHHarvesterHierarchy.svg deleted file mode 100644 index bdc6c88e80..0000000000 --- a/contracts/docs/OETHHarvesterHierarchy.svg +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - -UmlClassDiagram - - - -20 - -Governable -../contracts/governance/Governable.sol - - - -26 - -<<Abstract>> -AbstractHarvester -../contracts/harvest/AbstractHarvester.sol - - - -26->20 - - - - - -51 - -<<Interface>> -IOracle -../contracts/interfaces/IOracle.sol - - - -26->51 - - - - - -57 - -<<Interface>> -IStrategy -../contracts/interfaces/IStrategy.sol - - - -26->57 - - - - - -61 - -<<Interface>> -IVault -../contracts/interfaces/IVault.sol - - - -26->61 - - - - - -249 - -<<Interface>> -IBalancerVault -../contracts/interfaces/balancer/IBalancerVault.sol - - - -26->249 - - - - - -280 - -<<Interface>> -IUniswapV2Router -../contracts/interfaces/uniswap/IUniswapV2Router02.sol - - - -26->280 - - - - - -281 - -<<Interface>> -IUniswapV3Router -../contracts/interfaces/uniswap/IUniswapV3Router.sol - - - -26->281 - - - - - -199 - -<<Interface>> -ICurvePool -../contracts/strategies/ICurvePool.sol - - - -26->199 - - - - - -710 - -<<Interface>> -IERC20 -../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol - - - -26->710 - - - - - -34 - -OETHHarvester -../contracts/harvest/OETHHarvester.sol - - - -34->26 - - - - - -239 - -VaultStorage -../contracts/vault/VaultStorage.sol - - - -61->239 - - - - - -215 - -OUSD -../contracts/token/OUSD.sol - - - -215->20 - - - - - -224 - -<<Abstract>> -Initializable -../contracts/utils/Initializable.sol - - - -215->224 - - - - - -227 - -<<Abstract>> -InitializableERC20Detailed -../contracts/utils/InitializableERC20Detailed.sol - - - -215->227 - - - - - -227->710 - - - - - -239->20 - - - - - -239->215 - - - - - -239->224 - - - - - diff --git a/contracts/docs/OETHHarvesterSimpleHierarchy.svg b/contracts/docs/OETHHarvesterSimpleHierarchy.svg new file mode 100644 index 0000000000..7db0977ac7 --- /dev/null +++ b/contracts/docs/OETHHarvesterSimpleHierarchy.svg @@ -0,0 +1,60 @@ + + + + + + +UmlClassDiagram + + + +21 + +Governable +../contracts/governance/Governable.sol + + + +26 + +Strategizable +../contracts/governance/Strategizable.sol + + + +26->21 + + + + + +39 + +OETHHarvesterSimple +../contracts/harvest/OETHHarvesterSimple.sol + + + +39->26 + + + + + +279 + +<<Abstract>> +Initializable +../contracts/utils/Initializable.sol + + + +39->279 + + + + + diff --git a/contracts/docs/OETHHarvesterSimpleSquashed.svg b/contracts/docs/OETHHarvesterSimpleSquashed.svg new file mode 100644 index 0000000000..c84fabad94 --- /dev/null +++ b/contracts/docs/OETHHarvesterSimpleSquashed.svg @@ -0,0 +1,72 @@ + + + + + + +UmlClassDiagram + + + +39 + +OETHHarvesterSimple +../contracts/harvest/OETHHarvesterSimple.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   __gap: uint256[50] <<Strategizable>> +   ___gap: uint256[48] <<OETHHarvesterSimple>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   strategistAddr: address <<Strategizable>> +   wrappedNativeToken: address <<OETHHarvesterSimple>> +   dripper: address <<OETHHarvesterSimple>> +   supportedStrategies: mapping(address=>bool) <<OETHHarvesterSimple>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _setStrategistAddr(_address: address) <<Strategizable>> +    _harvestAndTransfer(_strategy: address) <<OETHHarvesterSimple>> +    _setDripper(_dripper: address) <<OETHHarvesterSimple>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setStrategistAddr(_address: address) <<onlyGovernor>> <<Strategizable>> +    initialize(_governor: address, _strategist: address, _dripper: address) <<initializer>> <<OETHHarvesterSimple>> +    harvestAndTransfer(_strategy: address) <<OETHHarvesterSimple>> +    harvestAndTransfer(_strategies: address[]) <<OETHHarvesterSimple>> +    setSupportedStrategy(_strategy: address, _isSupported: bool) <<onlyGovernorOrStrategist>> <<OETHHarvesterSimple>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernorOrStrategist>> <<OETHHarvesterSimple>> +    setDripper(_dripper: address) <<onlyGovernor>> <<OETHHarvesterSimple>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> StrategistUpdated(_address: address) <<Strategizable>> +    <<event>> Harvested(strategy: address, token: address, amount: uint256, receiver: address) <<OETHHarvesterSimple>> +    <<event>> SupportedStrategyUpdated(strategy: address, status: bool) <<OETHHarvesterSimple>> +    <<event>> DripperUpdated(dripper: address) <<OETHHarvesterSimple>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyGovernorOrStrategist() <<Strategizable>> +    constructor() <<Governable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    constructor(_wrappedNativeToken: address) <<OETHHarvesterSimple>> + + + diff --git a/contracts/docs/OETHHarvesterSimpleStorage.svg b/contracts/docs/OETHHarvesterSimpleStorage.svg new file mode 100644 index 0000000000..aef109c1e7 --- /dev/null +++ b/contracts/docs/OETHHarvesterSimpleStorage.svg @@ -0,0 +1,59 @@ + + + + + + +StorageDiagram + + + +1 + +OETHHarvesterSimple <<Contract>> + +slot + +0 + +1-50 + +51 + +52-101 + +102 + +103 + +104-151 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.______gap (1600) + +unallocated (12) + +address: Strategizable.strategistAddr (20) + +uint256[50]: Strategizable.__gap (1600) + +unallocated (12) + +address: dripper (20) + +mapping(address=>bool): supportedStrategies (32) + +uint256[48]: ___gap (1536) + + + diff --git a/contracts/docs/OETHHarvesterSquashed.svg b/contracts/docs/OETHHarvesterSquashed.svg deleted file mode 100644 index bbe36e0649..0000000000 --- a/contracts/docs/OETHHarvesterSquashed.svg +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - -UmlClassDiagram - - - -34 - -OETHHarvester -../contracts/harvest/OETHHarvester.sol - -Private: -   governorPosition: bytes32 <<Governable>> -   pendingGovernorPosition: bytes32 <<Governable>> -   reentryStatusPosition: bytes32 <<Governable>> -Public: -   _NOT_ENTERED: uint256 <<Governable>> -   _ENTERED: uint256 <<Governable>> -   rewardTokenConfigs: mapping(address=>RewardTokenConfig) <<AbstractHarvester>> -   supportedStrategies: mapping(address=>bool) <<AbstractHarvester>> -   vaultAddress: address <<AbstractHarvester>> -   rewardProceedsAddress: address <<AbstractHarvester>> -   baseTokenAddress: address <<AbstractHarvester>> -   baseTokenDecimals: uint256 <<AbstractHarvester>> -   uniswapV2Path: mapping(address=>address[]) <<AbstractHarvester>> -   uniswapV3Path: mapping(address=>bytes) <<AbstractHarvester>> -   balancerPoolId: mapping(address=>bytes32) <<AbstractHarvester>> -   curvePoolIndices: mapping(address=>CurvePoolIndices) <<AbstractHarvester>> - -Internal: -    _governor(): (governorOut: address) <<Governable>> -    _pendingGovernor(): (pendingGovernor: address) <<Governable>> -    _setGovernor(newGovernor: address) <<Governable>> -    _setPendingGovernor(newGovernor: address) <<Governable>> -    _changeGovernor(_newGovernor: address) <<Governable>> -    _decodeUniswapV2Path(data: bytes, token: address): (path: address[]) <<AbstractHarvester>> -    _decodeUniswapV3Path(data: bytes, token: address): (path: bytes) <<AbstractHarvester>> -    _decodeBalancerPoolId(data: bytes, balancerVault: address, token: address): (poolId: bytes32) <<AbstractHarvester>> -    _decodeCurvePoolIndices(data: bytes, poolAddress: address, token: address): (indices: CurvePoolIndices) <<AbstractHarvester>> -    _harvestAndSwap(_strategyAddr: address, _rewardTo: address) <<AbstractHarvester>> -    _harvest(_strategyAddr: address) <<AbstractHarvester>> -    _swap(_swapToken: address, _rewardTo: address, _priceProvider: IOracle) <<AbstractHarvester>> -    _doSwap(swapPlatform: SwapPlatform, routerAddress: address, rewardTokenAddress: address, amountIn: uint256, minAmountOut: uint256): (amountOut: uint256) <<AbstractHarvester>> -    _swapWithUniswapV2(routerAddress: address, swapToken: address, amountIn: uint256, minAmountOut: uint256): (amountOut: uint256) <<AbstractHarvester>> -    _swapWithUniswapV3(routerAddress: address, swapToken: address, amountIn: uint256, minAmountOut: uint256): (amountOut: uint256) <<AbstractHarvester>> -    _swapWithBalancer(balancerVaultAddress: address, swapToken: address, amountIn: uint256, minAmountOut: uint256): (amountOut: uint256) <<AbstractHarvester>> -    _swapWithCurve(poolAddress: address, swapToken: address, amountIn: uint256, minAmountOut: uint256): (amountOut: uint256) <<AbstractHarvester>> -External: -    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> -    claimGovernance() <<Governable>> -    setRewardProceedsAddress(_rewardProceedsAddress: address) <<onlyGovernor>> <<AbstractHarvester>> -    setRewardTokenConfig(_tokenAddress: address, tokenConfig: RewardTokenConfig, swapData: bytes) <<onlyGovernor>> <<AbstractHarvester>> -    setSupportedStrategy(_strategyAddress: address, _isSupported: bool) <<onlyGovernor>> <<AbstractHarvester>> -    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<AbstractHarvester>> -    harvestAndSwap(_strategyAddr: address) <<nonReentrant>> <<AbstractHarvester>> -    harvestAndSwap(_strategyAddr: address, _rewardTo: address) <<nonReentrant>> <<AbstractHarvester>> -Public: -    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> SupportedStrategyUpdate(strategyAddress: address, isSupported: bool) <<AbstractHarvester>> -    <<event>> RewardTokenConfigUpdated(tokenAddress: address, allowedSlippageBps: uint16, harvestRewardBps: uint16, swapPlatform: SwapPlatform, swapPlatformAddr: address, swapData: bytes, liquidationLimit: uint256, doSwapRewardToken: bool) <<AbstractHarvester>> -    <<event>> RewardTokenSwapped(rewardToken: address, swappedInto: address, swapPlatform: SwapPlatform, amountIn: uint256, amountOut: uint256) <<AbstractHarvester>> -    <<event>> RewardProceedsTransferred(token: address, farmer: address, protcolYield: uint256, farmerFee: uint256) <<AbstractHarvester>> -    <<event>> RewardProceedsAddressChanged(newProceedsAddress: address) <<AbstractHarvester>> -    <<modifier>> onlyGovernor() <<Governable>> -    <<modifier>> nonReentrant() <<Governable>> -    constructor() <<Governable>> -    governor(): address <<Governable>> -    isGovernor(): bool <<Governable>> -    constructor(_vault: address, _wethAddress: address) <<OETHHarvester>> - - - diff --git a/contracts/docs/OETHHarvesterStorage.svg b/contracts/docs/OETHHarvesterStorage.svg deleted file mode 100644 index 44e286ef68..0000000000 --- a/contracts/docs/OETHHarvesterStorage.svg +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - -StorageDiagram - - - -3 - -OETHHarvester <<Contract>> - -slot - -0 - -1 - -2 - -3 - -4 - -5 - -6 - -type: <inherited contract>.variable (bytes) - -mapping(address=>RewardTokenConfig): AbstractHarvester.rewardTokenConfigs (32) - -mapping(address=>bool): AbstractHarvester.supportedStrategies (32) - -unallocated (12) - -address: AbstractHarvester.rewardProceedsAddress (20) - -mapping(address=>address[]): AbstractHarvester.uniswapV2Path (32) - -mapping(address=>bytes): AbstractHarvester.uniswapV3Path (32) - -mapping(address=>bytes32): AbstractHarvester.balancerPoolId (32) - -mapping(address=>CurvePoolIndices): AbstractHarvester.curvePoolIndices (32) - - - -1 - -RewardTokenConfig <<Struct>> - -offset - -0 - -1 - -type: variable (bytes) - -unallocated (6) - -SwapPlatform: swapPlatform (1) - -bool: doSwapRewardToken (1) - -address: swapPlatformAddr (20) - -uint16: harvestRewardBps (2) - -uint16: allowedSlippageBps (2) - -uint256: liquidationLimit (32) - - - -3:7->1 - - - - - -2 - -CurvePoolIndices <<Struct>> - -offset - -0 - -type: variable (bytes) - -uint128: baseTokenIndex (16) - -uint128: rewardTokenIndex (16) - - - -3:15->2 - - - - - diff --git a/contracts/docs/OSonicHarvesterHierarchy.svg b/contracts/docs/OSonicHarvesterHierarchy.svg new file mode 100644 index 0000000000..ad89bf857b --- /dev/null +++ b/contracts/docs/OSonicHarvesterHierarchy.svg @@ -0,0 +1,86 @@ + + + + + + +UmlClassDiagram + + + +21 + +Governable +../contracts/governance/Governable.sol + + + +26 + +Strategizable +../contracts/governance/Strategizable.sol + + + +26->21 + + + + + +39 + +OETHHarvesterSimple +../contracts/harvest/OETHHarvesterSimple.sol + + + +39->26 + + + + + +279 + +<<Abstract>> +Initializable +../contracts/utils/Initializable.sol + + + +39->279 + + + + + +40 + +OSonicHarvester +../contracts/harvest/OSonicHarvester.sol + + + +41 + +SuperOETHHarvester +../contracts/harvest/SuperOETHHarvester.sol + + + +40->41 + + + + + +41->39 + + + + + diff --git a/contracts/docs/OSonicHarvesterSquashed.svg b/contracts/docs/OSonicHarvesterSquashed.svg new file mode 100644 index 0000000000..ef34b5342e --- /dev/null +++ b/contracts/docs/OSonicHarvesterSquashed.svg @@ -0,0 +1,72 @@ + + + + + + +UmlClassDiagram + + + +40 + +OSonicHarvester +../contracts/harvest/OSonicHarvester.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   __gap: uint256[50] <<Strategizable>> +   ___gap: uint256[48] <<OETHHarvesterSimple>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   strategistAddr: address <<Strategizable>> +   wrappedNativeToken: address <<OETHHarvesterSimple>> +   dripper: address <<OETHHarvesterSimple>> +   supportedStrategies: mapping(address=>bool) <<OETHHarvesterSimple>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _setStrategistAddr(_address: address) <<Strategizable>> +    _harvestAndTransfer(_strategy: address) <<SuperOETHHarvester>> +    _setDripper(_dripper: address) <<OETHHarvesterSimple>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setStrategistAddr(_address: address) <<onlyGovernor>> <<Strategizable>> +    initialize(_governor: address, _strategist: address, _dripper: address) <<initializer>> <<OETHHarvesterSimple>> +    harvestAndTransfer(_strategy: address) <<OETHHarvesterSimple>> +    harvestAndTransfer(_strategies: address[]) <<OETHHarvesterSimple>> +    setSupportedStrategy(_strategy: address, _isSupported: bool) <<onlyGovernorOrStrategist>> <<OETHHarvesterSimple>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernorOrStrategist>> <<OETHHarvesterSimple>> +    setDripper(_dripper: address) <<onlyGovernor>> <<OETHHarvesterSimple>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> StrategistUpdated(_address: address) <<Strategizable>> +    <<event>> Harvested(strategy: address, token: address, amount: uint256, receiver: address) <<OETHHarvesterSimple>> +    <<event>> SupportedStrategyUpdated(strategy: address, status: bool) <<OETHHarvesterSimple>> +    <<event>> DripperUpdated(dripper: address) <<OETHHarvesterSimple>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyGovernorOrStrategist() <<Strategizable>> +    constructor() <<Governable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    constructor(_wrappedNativeToken: address) <<OSonicHarvester>> + + + diff --git a/contracts/docs/OSonicHarvesterStorage.svg b/contracts/docs/OSonicHarvesterStorage.svg new file mode 100644 index 0000000000..d8a60a18d9 --- /dev/null +++ b/contracts/docs/OSonicHarvesterStorage.svg @@ -0,0 +1,59 @@ + + + + + + +StorageDiagram + + + +1 + +OSonicHarvester <<Contract>> + +slot + +0 + +1-50 + +51 + +52-101 + +102 + +103 + +104-151 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.______gap (1600) + +unallocated (12) + +address: Strategizable.strategistAddr (20) + +uint256[50]: Strategizable.__gap (1600) + +unallocated (12) + +address: OETHHarvesterSimple.dripper (20) + +mapping(address=>bool): OETHHarvesterSimple.supportedStrategies (32) + +uint256[48]: OETHHarvesterSimple.___gap (1536) + + + diff --git a/contracts/docs/generate.sh b/contracts/docs/generate.sh index 3605bf3e73..6065a6f6e5 100644 --- a/contracts/docs/generate.sh +++ b/contracts/docs/generate.sh @@ -13,26 +13,30 @@ sol2uml .. -s -d 0 -b Flipper -o FlipperSquashed.svg sol2uml storage .. -c Flipper -o FlipperStorage.svg # contracts/harvest -sol2uml .. -v -hv -hf -he -hs -hl -b Dripper -o DripperHierarchy.svg +sol2uml .. -v -hv -hf -he -hs -hl -hi -b Dripper -o DripperHierarchy.svg sol2uml .. -s -d 0 -b Dripper -o DripperSquashed.svg sol2uml storage .. -c Dripper -o DripperStorage.svg -sol2uml .. -v -hv -hf -he -hs -hl -b OETHDripper -o OETHDripperHierarchy.svg -sol2uml .. -s -d 0 -b OETHDripper -o OETHDripperSquashed.svg -sol2uml storage .. -c OETHDripper -o OETHDripperStorage.svg +sol2uml .. -v -hv -hf -he -hs -hl -hi -b OETHFixedRateDripper -o OETHFixedRateDripperHierarchy.svg +sol2uml .. -s -d 0 -b OETHFixedRateDripper -o OETHFixedRateDripperSquashed.svg +sol2uml storage .. -c OETHFixedRateDripper -o OETHFixedRateDripperStorage.svg -sol2uml .. -v -hv -hf -he -hs -hl -b Harvester -o HarvesterHierarchy.svg +sol2uml .. -v -hv -hf -he -hs -hl -hi -b Harvester -o HarvesterHierarchy.svg sol2uml .. -s -d 0 -b Harvester -o HarvesterSquashed.svg sol2uml storage .. -c Harvester -o HarvesterStorage.svg -sol2uml .. -v -hv -hf -he -hs -hl -b OETHHarvester -o OETHHarvesterHierarchy.svg -sol2uml .. -s -d 0 -b OETHHarvester -o OETHHarvesterSquashed.svg -sol2uml storage .. -c OETHHarvester -o OETHHarvesterStorage.svg - -sol2uml .. -v -hv -hf -he -hs -hl -b OETHHarvesterSimple -o OETHHarvesterSimpleHierarchy.svg +sol2uml .. -v -hv -hf -he -hs -hl -hi -b OETHHarvesterSimple -o OETHHarvesterSimpleHierarchy.svg sol2uml .. -s -d 0 -b OETHHarvesterSimple -o OETHHarvesterSimpleSquashed.svg sol2uml storage .. -c OETHHarvesterSimple -o OETHHarvesterSimpleStorage.svg --hideExpand __gap,___gap,______gap +sol2uml .. -v -hv -hf -he -hs -hl -hi -b OETHBaseHarvester -o OETHBaseHarvesterHierarchy.svg +sol2uml .. -s -d 0 -b OETHBaseHarvester -o OETHBaseHarvesterSquashed.svg +sol2uml storage .. -c OETHBaseHarvester -o OETHBaseHarvesterStorage.svg --hideExpand __gap,___gap,______gap + +sol2uml .. -v -hv -hf -he -hs -hl -hi -b OSonicHarvester -o OSonicHarvesterHierarchy.svg +sol2uml .. -s -d 0 -b OSonicHarvester -o OSonicHarvesterSquashed.svg +sol2uml storage .. -c OSonicHarvester -o OSonicHarvesterStorage.svg --hideExpand __gap,___gap,______gap + # contracts/governance sol2uml .. -v -hv -hf -he -hs -hl -b Governor -o GovernorHierarchy.svg sol2uml .. -s -d 0 -b Governor -o GovernorSquashed.svg From 666f36cf92e54cba225e2ec14408a813b3a6704e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 Feb 2025 16:10:36 +1100 Subject: [PATCH 23/80] Updated Sonic contract diagram --- contracts/docs/plantuml/sonicContracts.png | Bin 43673 -> 58456 bytes contracts/docs/plantuml/sonicContracts.puml | 39 +++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/contracts/docs/plantuml/sonicContracts.png b/contracts/docs/plantuml/sonicContracts.png index c5f9892b97d6b02d0944c881937b7213deb97f01..9bd6268fcd8f511c1b95795e6ceafb48e517a1de 100644 GIT binary patch literal 58456 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 qf(%p!JbSg-93`lqHh4=mJ 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 Vz6rHP<+8-zhYxBU$W%FT<)3n++yDRo diff --git a/contracts/docs/plantuml/sonicContracts.puml b/contracts/docs/plantuml/sonicContracts.puml index 01e5a4f430..0768398bf0 100644 --- a/contracts/docs/plantuml/sonicContracts.puml +++ b/contracts/docs/plantuml/sonicContracts.puml @@ -7,13 +7,13 @@ !$changedColor = Orange !$thirdPartyColor = WhiteSmoke -' legend -' blue - Origin -' ' green - new -' ' orange - changed +legend +blue - Origin +green - new +' orange - changed ' yellow - phase2 -' white - 3rd Party -' end legend +white - 3rd Party +end legend title "Sonic Contract Dependencies" @@ -43,6 +43,14 @@ object "OSonicVault" as vault <><> #$originColor { asset: wS } +object "OSonicHarvester" as harv <><> #$newColor { + rewards: CRV +} + +' Oracle +object "OSonicOracleRouter" as router <> #DeepSkyBlue { +} + object "SonicStakingStrategy" as stakeStrat <><> #$originColor { asset: wS } @@ -51,19 +59,22 @@ object "Special Fee Contract" as sfc <><> { asset: S } -object "Curve AMO Strategy" as amoStrat <><> #$originColor { +object "Curve AMO Strategy" as curveAmoStrat <><> #$newColor { asset: wS reward: CRV } -object "OSonicHarvester" as harv <><> #$originColor { - rewards: CRV +object "Curve OS/wS\nPool" as curvePool <> { + assets: OS, wS + lp: OSwS } -' Oracle -object "OSonicOracleRouter" as router <> #DeepSkyBlue { +object "Curve OS/wS\nGauge" as curveGauge <> { + asset: OSwS + lp: OSwS-gauge } + wos <. zap zap ..> os zap ..> vault @@ -75,13 +86,15 @@ os <.> vault vault <.> drip vault <...> stakeStrat stakeStrat ..> sfc -vault <...> amoStrat +vault <...> curveAmoStrat vault .> router vault <.. harv drip <.. harv -harv <..> amoStrat +harv <..> curveAmoStrat +curveAmoStrat ..> curvePool +curveAmoStrat ..> curveGauge @enduml \ No newline at end of file From f2766ed69e3171b0c958bd072e26aed6d777da7e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 18 Feb 2025 16:11:55 +1100 Subject: [PATCH 24/80] Updated Sonic contracts diagram --- contracts/docs/plantuml/sonicContracts.png | Bin 58456 -> 53105 bytes contracts/docs/plantuml/sonicContracts.puml | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/docs/plantuml/sonicContracts.png b/contracts/docs/plantuml/sonicContracts.png index 9bd6268fcd8f511c1b95795e6ceafb48e517a1de..895b1ae3f447bc66b74c92b4b33247755d1a9dd1 100644 GIT binary patch literal 53105 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%0D3?lxgCpKLdrbqo?GnHXsQxd%tc1@o_&c7^ zItGKB?7Nmd?$#(W2SSHpMwR1pNFY#%);_v4Tq3y`1WZl_+tFW>^u_i;=K)MHQEOBu zWhB(mwQPI{p)5y@ec4xQqOKvjfJh~AH2@EO&Ip@V-$gf#r*4!lWk-VuHmM;~od6Y{} zkQOV;@6!@pNigLhiR{;&%cZ%A!|pOW;~)Uw&ir!?MH5?u^b3Q zi~Vc(U}_)Xew_oSI!DA4eF)xKehT+h66-eocq|z<;k6W@cw~S?+H+jRs!smDC%xM3 zQ5Ra+IqHhlArm%X6EE@T7ycH{hQak&Y)xjKM>Iwba6y;bvo8j5-A)VAm$c2<*NQ+2 zY(XD@UhE@5besBoJE}Y$gn_fqbUv@~c6hki75Qe_NWR5tR;hV{;4&Bf>Hb6S2>NJYs z*gU8I7)=&Mdsc9z3M_EGT7eA2j?jbxx+aiV{}Ksvvbkp1kX!OdMk$`|EiUm1Sbcb< z%A)U|(48kwbf)9T3Y|cvqJKFS;d4AQA>c$q0RM!Sx`mQ+5rumgF7&%~E648>T$&51 z3~ZC(X#B;~1$P>9&6ceeL4s(Sm`)-XM1A8O?;!z;F#L@M=+$GPAsWXS5ZXXC>9{Ht zV}qCclOHJuhz9^%OAP!y_m0~!PYt`l_KN+olHjq1MUV3=MyRKL$`B zYmO=V;F|&)RBI+7rEh19Szv&L#X^exmiC_;q zxa+VDB`nIKH%O&uT}VjSKkk&PM)B?O@yx>D0HU*O=S)C77A>MTnUx(R>h^NjKS?q% zlEf5-(!H(z`}Uw`64!!}?Iod&TujJPVcN29hNc38icdS0M1Z6&4{t&w2g1LtZM>`) zRv|6?g&enQHPIg2*K=laA+28VJ9PX-KN$SYm{`VP<;uV#&L6y?lE|fDM<6jlLs*gO zJ!a%uZ2fDgSXMzXbDXMr3oVQBD%AexBu+U2;c!1*fENfJK{LqWjYeIW_)q?$Zyq)K1bKCaCA)!9Xi7Pmq&fOA+zDt;LGhNLs zC6BqZSa=gU>i@G#Fur9I#I{xwfkZqKzi%4!_=N|PeZ`InT`dJwy&LS4;4NDdsNGLr zP6d@~28qwPhG4bZa%JTZT%6A8o#f5OPUuYL+Lenm<3#=8(@BY0;KCdFh4DiYBB(>Z zIkrsLa9*D7eGx~9Ca{Y=+lgc<=s_7jEdA*9Y)t-jmuy!RP@GEPFUKchFJou=?(}=V9 z5lrUPTWVWRGLTlf#M^JUUhUf3CfRJ#5p5Duc2(|3SyA(1jwTOkGyygN>*4oUJk=(y z)VemP2Tvh-^S!OL(gyqz?1!wMZHhIw6J+UL_vacM{om3x$p566R2>y&&h+zLZT9hdvnIkEZT?z`Rk zZ`qc)uvZWzn(+k0&zwtP*)*Cd1n(8>;1^0M-WpWKZQZF_@;eM?zG(8d^! zq+YZU*-?1n+XL|O53BzHUM{DX@vY)=P0!V`cU3ZhwrGv|dsh*=n6IE*&IZh7^Mb3? zrs@G?-0WAi7qU2BmpEbX36eF-ihyk6(v`LM&fV{O4kTjI$!PzEcz`(_3Xh?Lcj0@Op5E)ev4Pquh%lA)oyaH;`qkHZX4LEdG z%#P87+>OWphv{^c@OQT6F! zA0%J$MBqp2*G3u){ax33hKfMsRFaIGO}zx)B;vnruH8up9R#%4gEU(W9B$~cMHn3J zvmX}Jkr}<>OldVgYs=<86b0_`RL&6ao)|Pa;Zu&%a)w*QSm-r_Zk97k$(7Me0QkIrwk1N*Lxo$(ufIP8P#-zYj@A$iWveV}LcloNwv zP-=@)tY!hw6O<8)zq>n&Ia61m(_MZsq4D8w6#_JxNJW9 zK0_raT_(_Mo&FR>9=_1$DvHqkzEx6gsh1ZoAtu8n-pkk|?!x5kT;SY0t~Ioh_SH4G zR&oXP=wZ`QB0 z_04m<7TwN6GeLM(+9n15xq<^5iO#BxIXS4Lg!v-?BcP*L<tO+(&*nkX0!O3^8*==ed~PD5#*r? zPrBoC)j1uP5}V18QG{-zw=f zLr|)C4=B~ouQIg!(J?Tn6f&edcFy+~BderSD*^DZsp+CfJ{^1!V01vL2Q zL5t%`CxB@26TJX{Og=z^*{TQ7NX>Wh`#mo*%+`}@3-Yo>f&jb3iYE?V50X&{pMle|K;d5*Ahp5ZZO3xwW-%1j+^(%J~ui?7W%=15pgX zxmu8T^X6o4z7AmVFp-Y`{IRmLW3TsBRgEty`Fnjmp20J%5*cuOd~DXC`q)2A>SKenDY^t<;G z+tpN41DPBz50BAoAt8Q#@6Yca6(}7Y9bxm}PF0bJ1t}FNOao#B0KiOU)bBYFNzTh7 zZIy978WP!l8`CpISW;2Zm-qkRYpM}%5L=p$V75-G61zbg@;GMV;>BeF*7rZ zp?V4jr;sh!0q{S~&00w-TBUVUdE(&(a6{7o&m>ZY^W!nUHnQu@U*}eLKs34@<*GVi z2m!a{qjzw*i9C*bLf)#XaAK&bt1GK*?dn>;m*xwg9RU3oN0P?#W)nn?U4tEQ!;rL8TUCiP}8py1`N zRj~gsFez$zF&P=kPhH2}`TF2Yk5`yvrrK7%`!5+F&@(b3rpX$1s2Zo7Q}0<$4X zfl6YY-yyZ2vNEbtQ9=SvK0YESIC$g|ES{a{=qWe@7#KN_xZ)8J5dp}{t{WYhf~G`Q z(DJ|#SB8a+P3ut|`y)-n%#7S2h6G#yfam({3Set$Y6-O=W-L@x@X6p50mHIAfVf>> zU(=z-Cngp)xqAZSB7HCbm(;-8PoYtfAr`)81V=>6z@RQT)iGhyXI>Ca1OP99{Y_C? zdMJ)UHX(Xl%yTz4EIgbeeLDuu?R95(8ljj~Lo7Ho1zFiI=8$8Av0~B@&2RFQwYBi@ z;K5^Bi~)J@fCN~`kDM6v#g`wE%7A?n4ei5-mCx|V2q16;4T<0?!Wanx#B&;1L1QB5pEEQx z)RWj%RZ@E2Jqw-dLLVS=UgUX2WgW92b@rKeLO&dUKiwX_oSl=Cv#_x6HZ0-xMbJ); zea9%VfPerE&9nQ@K$F10Sg2@f&fd{hBqU%~IFH#6f_>C8dw&%W0LvZ=feynkXM>vv zkOU2WROizgO@N#d0BW${2*bm|T+n_&_=sVu4FT$UpEk(Y*_kC(34n0l7@oJc^TSR; z2Y^`^%;f}#U?)i1%OjZgBe6+I*soqi<#Im!n-1XBrp6SUz|s7#wAxOtpcDY4kfEU| zsO)Dc1QQ1X;}r}GVdllQ6CuFD0%){AL|Z~YKDNjM`y8_5_q!kmXf>Y_oIuB9*tsb{eSxjIoe_XO!Nq@DDFVv;09L8sbj@4|5W!%QmjJv> z4v0rT^4U2!P!3g}An$-HJI3-2u3!BrWLv74%t;!C$H$va4i%0!4-XHw9u=*vu1=sM zLJ{o*rDbJ6z0r3r1_T7u*Rx;WU%AGSkdR>SQCRwDRO(sJ_wYkkqxlP<#0g_Vu`w}0 z`j)#FDC&eLE9t1^MCaBCp^81`2sY-yayi%uk;9V zI3h$Mf1>yH_9{^>uq(~J>t@JpIGykUjYYCMND|`T81L#LW zp9Fw)=Q|D21iO6h?fFs@>*0IAV0n6a>f~mufr+uOur&W9v`hsb1n_@|^xEfu#sJ=m zx8SxAHhIL{`a7#b_iI5;UteEaI~5BVbPy$^r-vghi@<_u`nhvLcO8aa5hzMiQ?kk@ zkmClhY9JCoaX1tHlTipHb#pWL-v)igqM@cH#waZ*=`Muwufi}&0-XZ7OxJ<}p)v*Y z*&B$lktlROqk@55PlOL4)_3aM5&?P^nD)Q!kD;>n5^N~|VwU^q@lp>f(90Sayb_gy zz_MZLKY(_u;$rGuA}$h=56F?bjv}jTYal-enw+6ZmV%R$lZ|cqYJh>BzCP5KhXufx z{Iwyy0pM?AVo>Q`&{xUY+uJKCp;mT11$|Oe$z~#4dJ6&I2-`lgZ>}WJ22Ra>i6dHVa01*v`34xfyIK2RP zaXWR?!oIz~3G<683|U209$)>I^P=YJmf0N?n!iVxcfbDG^q{!dJj)fa9Pd7w#Y6pb zNZSXlLf<_A-aJU@)c+eu5PHmjPU^%zLs9(D*JO&|$fSkaS-M;b_4SmtfUU)ydb!WS zPu~b8NG2XE1SIUrvG*)lNQF-?upRZ3$J^ZkKyRm_oe<-B7ItV0DSg=P$hu^U`i(N9 zuueSf+ic9m=n_p0%JU8iz@7{^8S=6r?hnut`XDpAmz%HQES0MyM@5yv$KEu&O!ZuiZB+UC(D$8?4hHZpje91ZtU;tYie$`;?=Vo zyo*EwjGwJ%s30A{^FI+H9@#xp%(&CxFuZ)(E+vk&oHtK z8s%T$jja|^V?um07#IcMGDz6jm1kzi0c4VfM$)d$#Q)!%vJj2)9AHBi#)Yw6DP09C z%RrDK=`y_m*hDozck*apn^XMeZDA>gfMjVJzpW4dF-eo_c_zJvK)1SG*Zd-c#g@zg z1M~|j<$Xo@D}22ys>lMl4b7--Zf*sHZ&|aF%?_4YW(Wem9e}>?{|yl>nmBY)!YIIi zfsU28ZZe=VA2?qV#B9_56MoLQpy~-EE(Z3B0=Y_L)9+_%g(R{I)n)!Y$Q?lM)@O5e z1hKNm(CY}vWI;|1hPx9^;F|LSEuVdr6V#{)sPMpjfG8n7QGX3eXW_nYq-cpfkBV*` z?(g?9;kDEyukKOATf;cslhWoPgrIv-;khMheGJn2_*A*tqz_$|evUb>Hz!XEO*vFc z2{i#0iSnhXk}|#a9<_4b&FVqzh{gTAdrJD3GfqaSmfzPNV{2~?044ZB;^h(#+aZ_} z)GGU0DJeP*>6O;k?|inni_131g3mqAB8f<$2xV~H!Af^I+a)6B^Yu_bSfHgBqNiUl zR5|`yCG!|Uf(vG!BNPx6)ah{|!8Xpo$H&LatmMZb+(LUb#G3RpjZ}so^i|N9J|f1Ird|YT?9j)an?D`WCC;hk4EOBMOCx!w|@g( zV6Z{OyuiI9ARu7Bd?5hdQXoH5_BW$U6}tO4`e}08aZf*`CmonENvb-W?%jUn|+9vZ*?PuRo?Cy;?BV8qGdsmYY zKPxSGFl0j1WMIM|%&_nbkOcm>Vas)!0HpHu7xB@N5n1S%9`~rZV_voMW<|-42aGQ= zh_z98y73}cT`25@W{_aR(>23fw=F*oEVQ@8oQH{&;DgYWF~u2ODhBBXc6a;UKDpXITil(^mx$^ zeP_P*K&1YZXv7ma(9p~$@3P=%UByhTLsE(r<8 zsL(iQr~{Drj_I6Zh5}Y+MOmU9w+)nBV+>B0SI!S&lfC}?xAkBAnjO&#s_8#j-OFi5 zS(6WQgUOM^!*o#zx#Y`xu;MVc)5LN`k02TySipb31OrMo|BRE&_nd%P(Qh0iN0A@p z6Qt6Sh06O80>JM<{z9LOoO~VB#J`t|w6SVeTSR@xohS-++VUNm5x;)QCOf2o!Dc*t z>F)9o`BwXqmbKZd%jRmXm^%0ziaU_)T@k?=_28Li(JSle$TXqEh-cT>NmTUbd;d#4V@wx7Fl4OK~0E z3jF$6VMDW_lYWWrnGO)r=}mYQksP(TIWK5!Foa6rVIXERMat~l4b$KT9m2;^|# z7h7^gaF+S5EJ`B!a`lq#?~~$ukpx5Wn!;Dg^9QV<0z%knt)*>M!vLoYmF$++pn(Yv z&SotYgz5mBs~HU!tNZdLZOUBu>)t$U-O%=7Of#vRk3On*3=tXQ8;l&BXc@~mqVckv z9+!0`-BO=)>P_SQlsQx7O`c`@FwSw}yol6FpI#cA;;`y|l{>uxJF-xIX|v=qZ(`}& zHx_*C{)+lVrcSM!SlrK_qy!#cDOYlAb5(0=Ef{o7%KM6KMXP@GjT*THhL(lvRwO;! z(Ab-@8t~1QraVw;uKJq$@5XzA+zmD%VMU}R0RJjb84BQG9+9y{yg#em{!&!qDwymw za&FhLXvCQx=$aKxS>w?O7lE;HXwqtG^w}WeNdEA`{7qLpqY_VtWc812;Z;g>LS^ER zyH5IJ66@Dn+M#gsJhdggy@ALiHWQNZfzh83>AKJa|0Ynp^3_r)FKJPl4GJCHIGpop z!Yk@2(WKbtb;a_YVmVIPPM>e%IR@rNCM`$GT~q;~yKR0*X?VsJJH9oGP*&Sr=X)n> za1c~1b9S}?WCj>#&MXg*RR|B41$URjYNA$)5+fXCu;@Ct`m`dMoNSO6#f)gQB7iAM zhLyl;Of6^DsYHX~i3NThZVX9pX|V{9FA8>tk{rhvQL`(!{jO2;A!Ql8 zUeUQrS?ORZlVyd*SNG{gdSWSO4eKGP#`|%?(a%I%i?+siFnl1aa_;o$otm+*fLOB9`}k1@>EN=cLgo zb^6_C;;JNdxrrs|b1+TuU9wSH<4N{;f#qY3spNGlw2dXcWXAnTaXBym{$OHw`5)m; zEco;ZWYcUK`qj@#Fm09go@lh8WRj*Y_l2L$N?(rahF;fd8{(Fp^;J%{w*)d(@FH-^ zkK~9LW|?kx$__KD?CGJ|`j_ZuHw=GW)(C@pYG*Vhfg@FTMyT%cS|=NyMq4nNaj5GC z!K-0_ZO}F*%;%r<836n_h|`>jhRa3j>kq!Bc@!A(h_9F;f1B17vAEU{_3yX)J>gUI z)DPbf-EeYHYipwF1s>)_f_0ebBCD0s;IqbBC&!^S&TV8~Rz{c8XI_!wv|@I0G|Lpp zgt0Fax4El?v@I=L-}-WGA&1Bd(w_fwvaQPCfRcq1<2^n@*&9{f(*7j?KeKu9`NyjD zq9TGRcFo_g0Yij3(i9ar9lH6N6XKa?o*SAyQFL+0BW0^#>Gd&hQ0otq*f}Zv&W=>r ztrpE2S-IbzX&-REukhp7PYhQgovfA~@~V8amhxNaLz`oDmSXt1w814(CT%%SA7$|; zlL~wpQS0j}irhv_*47q1HouVqN7j^1vZ!YAhz%S`E z?%xlrY=bV|0sQf-!=}5*lB)BCCy6J6QgyNpcHE>vx zu?H8p@titRdaX#t+jW4|P(u%U<C#kxrT<7k6vmEmefAR9cmkKY@;{f##ihY*jv^I|?pnRQT)m)uAWIlMfdne*rNy=m(o#y61^oiw`7 z1`MUrEVsPpqVDNV(15NZxJkUF%S<^%nGb>S<~jabfoAsnLLAc?>aOk25Ywlqf(#-U z8xO>LBSS+UKn)-2D}Bp*E|IQ@3&LxVb$}Gpp~gXno0}VSJ?{25fP`gpTbpLk90W%I zab6}kKvDtPXpRheIMF2smsob^IqD z+(M=2php@25vL33XsM~4TwTlmb<315cfRZT zci=e!cxqqN{<<3QT>MC+skdWFT~1_X#l~w&uyffRvyd(9>y40q)V7{O4pGFxH!XQh z!>L%^3Sz#391l~3VVA<-pgt}&vLn^{X2v!|`Wa(QSrh%{fg$LAc5X|ukHgelg%th-i z`TBIF{dMZ&%N4bTT*elMX2x$wQqmA_ePmtwf3r$IUw*Kx;Do>teCg?-RrYVc2}Bt{ zVCzE1TR_amsZFT6M-PI1&awMkW2PrWJ8qfN-VWCX_$EGuLZ&n2KNpD-{=uRuO<>BkQ` za?95_1*%p$)=X`kGte3Vh!|N&%`+sLlI0ASS6s_i%W1oZH5~8tfoK$#%g2V%fQ$t4u}Dl_ zlsJKt5Tg00J6oCA91JFKT|c&{hh8XM_8ybPMU#8zw@*AoLkq9<^p?OIYOZ&zKHlE6 zW??M9@ytl}Zp4p|(kL$JNOyU@tFqkqw z=o)1WKTI)07EuvJ{^p*rzScrIgHC3HIv?l7#Ccq%)})0!5>;!}MT=BhOaWq$$`HNC z>E(`Rk4Siix2gajMbF)%I7&07XPUweSOIsl)p+{z@{pl*Ns>#R!q zCfu`8?!6UD6S`e`&FLI`Tqg&9oYpjQMGP;2U$;SaXj}Fzj@JddE3It{$vceL8dNAj<8qy={+!wjPF+6u!(qW(7DO)aG~e=RA$rN5PopLfPEsAJ1b zoQ5PTfe&qk1YTJ?K0Es!A5O?@1f%b$ZZ(m-nWB42*taZp^!vnWi1J$F<0jQ!gJx5A z`DJj28ZZ5~I*iAhQh06)=h!y0mJHH$O(j#rl}zF1G=wybz!;_WDT)Q^_TFUv-WF;+ zO*RqH)mA{K->yjSqI0MXGnsEi5np=vI0l_Yd2w;)mo!Dd6_T8+scwae&t{&}oF8!^ zH&fDLjB)-kLXh(fMh1V1^y?M6ZTmVWM`sendEP@ca=#EZzv?EVWj1o6XhGv{Uo*+D z+aXVNe7t8=Cy6M$*YTN8_S=}+Fp*?#vwtqfcZ7!DTvl{jA>pw#no zjpligjdqC?H9ZlcQRbNI-y@Z?^cPu6<5;W2=c)#BeA1Fd!7}-wB;`%UbDGi@wj7Ri zAy=v%ooOZ)RegbN+iCM%q+6=yiMtl6I6ThzhQd-NvP?=E8ym=h0j(^oOHE7b;^Z_q zK0Z+KP7}%Io&;Zc_O-@7PX%v0Qpvivb@-MSd{Z5+7?4As76-y?cN3MVmP)+ zs)N|vjJ6@2V6Wy%ox)6jID_#WBPDen98bN4Kw9X=LC?-w+zr_#o3^sm%XF2%eg$+P zG78d3T=c)maBimi>Lz3F&*)LGD2n!O0t-*^qK4R^qAL%c@^ar_z`DqN%q}$_ckKA< zXVu;c>-P&_o^&*%tW1cCBl7=#a@y+Li@?rvJc6S-OU7Udw8t%7&M{ViCNs8P@bK_p zu?7*~1wfl^Ksbcd1A1D3I0E}*3|eC#$vP{_--v|>|A~&QxPT2oL`H@H^dR$qIgPrx z@s^Yv09|#7Zj#ZL_zmvPxNP!e<3tY@Rf&b(pq-nVTFS2yJpDM^-(N(q0S{e)Z9rfk zC_e3+5E3(3&DR#*S#`I2oJjdwjAunM2|;Y(X&M-t6=n(Er5ikHx$`9vBnJAIm$TSI z?ii@4Kj*^(qxk+V4wTA-Fhe`{qlooj9K3AkOtegJ{2 z-Q+-c@WgrNTM3AYGP}M4QozLeB?Zq70~cVeASSXasEN_7^a zPH&)_`+lrPj1pw$j-&PbL$2muNq z#M6LbZtv*m3&QvzOo{!z9&X6-DKw3AB@5z*;NSs>0zP{Ta!Qa%z`RFKV$?7FGHnYI zFRN$|do_8ottMNTlk>T+4-m+}Hv#1YR{iPc2Mo8rg^GcwczC?NH}96{mL~}juT1!{N4jH?iLN;wg5B+Jw6kb06hYk!4+cAaI*j0oK_|-2Vh=@F3SA# z%8FlK7#0v0r~~H?4y(~_UWKbQ3HU=0=SFxQ;tMewy+=M}1k@Q2Rr{V5ych^rBar_M z;yI8n8SQE`to|YeDpOfAt06B>qh3o&8n(DRy@0Sg!3d`lKe7Qt3L+wp9KZ{2kOO9_ z)NA<|`uh=d4T9(Zod9@_{%g5@e)r(W{%gIidvl;$1Q84dT^$ZT0&7sHI(_sj0XzIjc(4BUT2QUH( zO3GD1HI{H3rt_ntSU;RpP})g<0$G3(mJ-L_>gw^!&4G7bc*l?z#lt zjvcg8a&={|Sj(h=$O%j_J@{+^M=?NksQV#T=r$FJWefV~e9Hk1$&)f`{gFrSq30m1 zF1%sv=x7K^_k9DSgM-yHL2@B?6!5hT4TXuPr>^{%kbQ?yGRDNh0!d$hs~Fxzc6<*i z|0P#-J1bpXfZc=82cR5InXHw-7yeX3m&Mn>lULN~U*NP3Aq#M9;#6I`0ABzNyLOmMR0uKi{2=Zi3 zRQ)?29bt`r8z5nNdqV&UtUpJgGDZk0;()8AFB%-`ROTBIx7hf2HnTxg6`~r7eh`m< z8@)45PD+|*K9aQbrJ#!z@>eiApdv@|-AQfxu50xz0w{D|p2I8^?vRp^Q?e)T3}fAO+q>%oX{rpr@w? zY|4-*XelZE4GWZ!jA0z$W%#8H6}4Fin1tvlA;{T+*ID&XjK^R(QrlVCjehqJ>ryfp z-gi%+oxY}|H&8EuS84t`W@#A2@Q5Jdt`dat^!Zudu=(Yx&$j@ijXF`?OFT7!k-1XQW^D!+KlSt~VsK{^g`E?&M1rufqO^!^+Umd24kXm84!{Pb_QFq#@ zTwhV|HIowfl1J#%qLNTytw*#{%H671^OQD;J~**M5rlk6d=uA)U4A>?XqHerCUU;r zKN}-_$|w2j#!nVKof{Ekt`aF&fTz(D#%(M`>}A+5blibWWP~K8%K02*tC9%lhv+Nq z?i60WBLBKbQ*Z-lXCe1So{E)qYG!7pug@GjW!jA2S}s!8iT#)pfzJ=E6$Pt@skSk@ zg>QLy!oGUKBc?alW(I4i6>%*|R`4pgI`IQWz`|KzZ1*0+%U=Q~6&3YUIW-eY`dh#; zYCVgqK8<01o9c8LI^q02=GgIJ1KFd%^Sb!>>4H92IkGW~wiADuU}r0P(o}oAkbBOv zVY6H0tl|(?hNr*E|85`Ol|}IUUQ%mk1X`exOV49GkXH!Acz9zo(T@B{86o(o&4Q)HXiONlU}*$Y=dBZ z7%eBmL=H=M;D&ub!?TYqiPih8vfGCvmatOvE8w;Zx)=N_=77T=)4?p^4f>uM+7)DsG3J)FD^~)inP>E*Cev6bvZXIzhc%cI;O`8!FTTN%mh!$ud(+H~S=TXt%YkSiu+0p4)T zwbaI+nk(EQMFK+$`=bIZm74s>_G6M`%SQC`3bU`XTY>a?N~63VYo6^Mw{z&nP3JZg zjMRM{xNXu_v`l8~DQMWkTI~JqXrH(|(h|>Xfrco9>6S@ob>jY#2Et$WFld4C$h;qm zD$iwGtLfh5@UUq)hm|7sL&AQ7xVPIMZoAv-I~cP~`65?zak>1=Dy&kU6?%;9QnD)_ zNswQ$7au1V_*EZ%*|WEcx%trckaq!aHt-|B&5WQ5Eule1o4(lzpN7VbV}i?27FecoH^)#&K&4% z=Rft_kWarr6tG$jj!4oVoWobVbU4%N?65>2vye7H%=GMQP4l_+HydCYer!2snDjyHAzuLqQn$)a(PmSo$u(HV7Ei>^qvSeuZ~s-fW>BRJ*{+WNU`r0a ztqgI?{=~El;)xfHqZE?b>BL&vbq?1H(-olAA0hz-qll|NMf`^sTJHWL&P2`qD{P(U z*AR8gRrnq<6bZJ|z4WoS!ic7CxQ4sl0M$7yE~9;ka-{7XBO~IBOMf3MXkAPH#|~6G z<%nF>YXAk@o5v2eowTc zX#IArv7udZBIzOzyTF8q_@?fs(K5o!`iZkjU?9J zYH^{`+a%TFXVRI(t+OTHju?*Es5f~+H_i$b+bZV3CdPZybNg=c_;#yLIe08Hz;s+& zH!2?9Rwb_&#D1H(gqm8aF*Tu1e`ezpW?%^eyX2=!9nPSq50!$<8RFEjGfsJm+x$YD zREM+wTd9gk-NVuYLaiWZkBBILFyJXMuqnN9_5FX0Vs8Mw8Xoa8eF6+V#j~lPjwwGk z=+O%er*cF>fCx=h^?AX0*-{F0F+t__pJXy6#l(w#U3n-NKm0!g#Q*jS|DXPmC1|BI zB{XLr?UU2-q+~Ack4{J{C3ztLHVl}n#)5YbDyI;-lin5};99~#2y>p16cw=_u*7DXov~hG< z=JePyR6c`($QnoW%@Dp;ZMOJz%2tf9--+BgHPG7M9D5m^bm68?(dywecNgV6gzS1F~Ua`$C9 zUdA?Nld7B}yh-rb{LDG2+p==+gXnneX<;yCU&GbkiVX^L9BMy?=kD}paS!&FT}o)} zr*#+{p<1>)Wf=@>O@aNc4zsP)8bC;4qXe}#>@CZl74@w6s1 zdK((6ThgtB^~Joe6Olrs#V(U$|pH}8Bo02-uEfO@8KDx|LU=J7p;%y@N6+k}0H+3nn$B#)U{ zkb+Yz+7Q*pFG3(^A@dK!E23X-niHO??I=LRdDeY8TE5FWG`wdYhVpt|iV}T=Ihj;6 zeq}1>0|x9sg*R_>;WT&& zy7PYBATkr=sG<5aMf~+C)8hTYL6*1$GUOev@l_`FXT- zVZ(mKyO=a_c}lMOP6q!(v3}xwk#(Eb>*^#eF%1J>=M5lJ#U@f>4kkolq!3CgKM`&W z?cEK}msznN(#tumz2{uaY?t(vG*6b}l-TYFN_JId<&3qOSk^6PUv76jcAv4;jfj5w zPwpmihNp5S%rqX2RNk&46)ef>0&*&6!&w_3iVjCtF)I`ecZlKq(N?XSt>`P!VU z_)~~F)Lp6OLpko1BJh6Z9{uzdnZAQH3hnfhIgvDW@!ozO%fl`|qgFcw5zm<~SecEm z3+4RH82{{brPv{54jX(v!bnpOY<4xTo%D^_ef1me|89)bSs~ zs(G8aaHcIgzGpAPAvI5NqQU5lgNu}g)!pdO^4Z~CwDE(NP+TE`g%4XO=7;ShE*|90 zpW*$ZWW!ljzn<--9EUCHr3o9?%#E;U&Yu^Ai%ISMq>of8L?pATF9g?Nd;1f*mSJ(3 z36-5OqWF^no|5|g9vL%C>5Rc2vQjcCUcY4I`I`hcIn~N$yTFrbsA6|2pb_Hgq!1YH zLzgSE7I*h^DpH*l=cUU>LeF2ztGk}VGkpp|Q$YO8TE7G6#wXJ4+1NZlB9f^c8xJq5 zmkt~Npjp066Jv=e4db?%m)K>rhDEmI$@!}hHGLDe_3u`@e}6eqSH4iG9^JPPI%IsS zwjojb7N_$pe@Kq0lbiN0qw!h24bp1p>o1fPNG?zMSqO1EVn%_;qVt7)WsIL;MOyt4 zS%~jK9qVq4%|U(sh)?~rtdz=@3zs!N5-w$ zGrK;H>wMk!BYGw?_Nxcgn-7bObrt?kw1l>zp3GehOk*7u@5SA7W|`-E$p%S&q7pcd zG(;9rDB^zs`_D1jC#JUPcU7jU4^8=0&Z4<4_KA8r$DK#O9vLnlO}T)pur^sf?(Ybk zU40{WmiGV_wsKmI4d1$14V<_XXTyC=q-ipZP4}xLNnSnf)NWy+4e9{r$T-ed&ej|K z3y;mwHGRJvE8f;>`M1`f_vRj(gyf?Lnkx?$27Au)B(o z&4tDVan_YI{S%&w7?W(=jrL4%)LV8CWi0#I!m^@-)LwCi>#5Wo=uYnZrK|}U7qk@M z+WusJD^VURe49e|oUDA%3RDmGVBh^E+FmQ=7BG+1tmk|g0XItXc9p_bHr}_2{C0VV zFfh7P7T%9RfctM7lXJ@+ZLjn7@F^E%up61GRlRGC^u1E+A#nLAi58OgBwJxq3Dpgc z+j*ECS8GZJO}*yoPQg<}dB9 z7*0pG`ND2BkF&a4br#tfwmao{e^2O$r~-OIutC(~{+MtgJm)tR#1!;jD<64{ka@!4 zPF?lJjb*Lg6y-(z`9pQAYFCQ;MqE1p%fR=q_{_xdkX>KJ>tO{&@pY^UB)BI?Q~99K zP_z(buN{0q^1-psEuwQEsLNjJ0UTrgjW|$5>F+n#X|Ck;Ts(bOJnK!V+xXuE_knwq zukKzis10L6N2l%bB|BH0%OBU(7cpm-`n6pW)t;BxZImOT#r)|d$Xfhyz#}Ju4Rpud zAo01JDt%%bplRs;?k@%wA*Xt;o-&$o;4xRdB~$v&Li=|lQ9>jYLJx61a2X5XQvJ|m z?@cDB`n>y$|0zIE5v4)qkgiB3+&mLAy{QvnLZmRS;RQ8<#U!eYurLI=_M2kca#|{Aw58TMcNH6(n6N=Q9sCz?|H+>}jKg!D5&49RIp!2EtF^?8 zY5P6(5*uZ?QuVg$qq07v7us;Dh{mfstYDSc#^SBsDs;R8%UxQ$`u}y-V3QzTJgl$> zNAWje>QDDo+^s`=Uu(p_x4S;u8G!6gp(u9h!y^9k?RM~N>v0$-y#I&)WbP+&-2b!b zHVu*z`liMVD}~A!|4ePQQIH3NF7`gt_L;d!|6`7anzX<-VpyNaAt`3wG7-d!=cd?Q zwj`VPBOmQ~-A#^CN&PJJC%djdlmP-CP*g?K@uyYrSGoVxZ0M1SZa$SLU#U_QDEUa= z6JZ78pMzfd$Vy4vcH*M|ed$MY`|j3Uvn#gF*ZQU(UVNpWx2wg_ER*5CW||=jQzOz0#dmsHA1?wVnxJhImVZMKl%lQV!Kj5H6b9w2xBxTLoJbeBARF7bkwdl<3lbv z+y%hdxRhiOjrniZ$8h|QhY{{5c z+>kEtKaFI)DHi~^Z1Y1LYK|d6Al$EL5^x(Jag5Sa>xp^4XW=N-f95mcJ2)iES7^^` z9?&etJIA4*Kl01lMb<}ofpX#*f$~7aeTY+G-igH-*^Frn`bKpbId&IN@k`$ld3jhW z$J^^;BSLe+AojR^jO1yF#H-nh=Lo8Vqdav@9fcy^zH!+5S|%&XG8kR_RNDU_EV?+> z4@Jg4J!p|};Af>J6|7y%XbV%i8|NxUI#4vw39ohv`!7rTiz-wH*E%ujMtcpTfpSA``^bC3Tg*vyC2Ns1Vf0W(b)}LZ6FgePi`BqNGZr5WFGdW=45a+R!cS!TL z2rd$M;9y)3OdIBi9F~hLGP3=8N$}Z|#hu{uj8GP%$1@Js(&~)_)D!n4haWKwn>2C- zUxA^e!0S+&^AVf#l-8c`{BKD&z*`cUA9z#%v9#C+#$|9H`SZz|5+$wbvefN#ci<>Rmxp87ckODTM3fXVRWe2q4Tg}E zIrEs5%wvN&LnSH7kSQd}d?Ry2s3amXlSC;4UEo$FlZT5Fk{yEBQN-WzQ09~fp53E} z5DAS2%*3=d`R|FI69nPEkVf9{<~6jFkNGksQyhNc4K+HVeWujx$gd;g1HuCfEuJ7N zrs8+zSHxp`J?>%4Qzm*rp}CF4$!1|ju74b&b}$?|Y_a0s>PHRT^9 zop^GOPh>sq`eV`SgWLnS6~k=uYm#JWAFzi$+rKSB{QGB}-wt2lO%Sw_J z>{4v!$qb*?``&B6(EMjs@csK3X~#Vp}zr_c)&|$F}}!r;gg{ zG&dYEB`!H``u@fQ>Nd_Ro3*a)P`t6;uc(ms-ri)j>%8W>Gk?%cG;yUImrKp*nHDnE zx6tt2emgZ_F0_BbM5}}{&W*3w<>ESa`LStNw{D_O-hV6N!#@>5ZKJ%nX!MB4#Xx>qBGpY3 zmvi;>r`%}{RSSOwxB4wg0;!w-^N`J=3Jn_CZ$mZNIF;l#3Pj6YO5IAuMM{(v_OUFT z<}g}N&P~YLpV~_*dhW3Ntk{`9zJCFkqBjJq?G~bXpVflx}E?1QCR2g z>~FrWJ4_>Zex2=;T=bP5EA}QNeIoF+a9gdF}o*q!H*xj>mHFw%9L(8|zr{=bajForHcR4!_ z{^-xzl-lH8!{f!Hl5un0zsx+vUKGb6AsFs0s~0Vicc^TlESPF`syGXI2 zI`m)0JHgfJ=Svddvapom5A_&{UsJM)@`JA?R0dG_wfxDzTpp-K-n0-2dznp0;VSJD z6Vsuhj6Xn@Lu}+GCs?fL8zr!es9w5of>;84Gbynr za-NL;KISF5n~p9RuhmVxx`woCJ)jTPb#M?OFIvLb`B&rtNO(tHnc1^>EayyzT@B`fegT55>UFhv&|5 ziHmntR(^-5dUZ)DJY+GL1#=xn*YWJ%pNSR6BmezXN$~QA3=%i8@la^J@gl!kqgX&d zs9IBoyH0OhclX)u^?0&OEVK)p_f@p0j?qWcI|Rl+G5?L5c=4|FZ+O;52S^GFYb-v* z+l7Vg^?vC(#_kYGjaq<9xYNXUZq!8rdO+avVDr9tRdgE-4Gk@pC#@WLvQCV+WwdJz z@A>;vc?s0`Co!N+BHz}^Ds$fb`@m|-YYPEAhM zP=bu&(p9sXy1L=c+>5AMpR3q3K~9Y0cRGl>j@sGUmRdFK8I?hrcv|YkrEhCT?@7*% ze!1%E`iuLd>-(mr+?*Wx|M`3okvt@Z@9#RYg_`=Q&!BR1meG;!9p+}{=1n63!NIA! z+Me5gCe9|J016oiVpYyygi;u{s@?LNrY2z_p)E!78}y!?-9$&1daeKWZ#2m^?WQL- zheGfjCLC34wrx!djg5^}(U+IvK_L)&*PYrl(p6YFLZBRqMpF?Hkr*-CcLL5Q)wLBI z92`*SnV6V}%Js6cvNTyiqvvnWPUG}kzx`BvEqmYhHKB*?yP&KUb?iB7F)kT`EIXlO z8b&jT4Bf1Opov*i29*T;ODFlJ9H~VcwD-4(7{g}wLM#yMUt#r zQho#_oh*iE7oUC*8>{RbyZGbuonSq&Op^-lxX2ugFQ!BUt|CK-Kgm z-||d}BW{L1mFQ$HvbT+{kK*D6KNdK}(gz236kopB{iyc#a`#RvI>;?p+;pAj#1@J1 zH^cA4cs9uFAUzTy*VyZsL_>#L$?P z!^p^pipn3IF`FuW$dq}^ys>W@iJ4hm`iot-0y8QiLek4VhXXB|pG(D(G5DKM>gU^c2hs= zi+79b=6ho#hJfC)#pSCwp7E#SX!ba9 z;th5KQ(#ih#5YXY^_G9BtsTP3e2kZNY)xyzJ;4AWvkMnwXZGzw-PniNBf)u1)rN&O z5MIp=wb?>l*2pO-OWdOtgO_f?){UsSyCmSG==-xFZg^h(4qpi@8zbk~@XYCIrJIY8z-)sWAS^xlp;DFGVH# z%~efGI80K0`j(V{9W1waka3YI{giV-E=?`pX;8D7^T88!V@qgY77}H(#UPxUo$Rlp zh2SkHC?z~tdii0y&&rZ(^rNX?9~~iJFo~DCb~ygX#rhaA2q#P*|N0oOm0avHVu-FH zkWOeSeSU6_VOzw=`rVVZ4t~GOz_Vk&zRV|X)tHlFebDcu9Zsf! zcY68l!uZ$M&B+s6yU^R^2Z#R_wl1{WMIVSV@1+GaW`L#FzM!>%m4jn!aFCF!2{n^9 zJwcXb{Zg(#BV-}Ik0sr&e0#IO5Y6>Pmy~C^v84C!-%l43MPJY>S75iFPGblUBtZ{j zVkjvoHJ$6Obv(CegG$i6Iawplu*l&H6&?F{Fz%eQi%W`9M{RAukQ)CYF62wKet%!e zZ<}Bdu2bqrPwP3Nr7*TIub^OheBABN@92fU7-aC=y5+XF#|dAj6brA3K6&){&~a_6pgV7V2;NT6(758CFFN#C=|+$Nqnd?}e3YLjpt3V(P36Jl^gFrIMeOKDMV z?nH!u9(BOpEZnUZB_-YqgQ;d}n7k$#>Ux0FQbbtzi7K`vd6(vhFzzpc=4xsJ9C8J! zycAPXJaYz5K(_>94wTnR&v}t_Iy$P!g&AAOjY?$Lt?kj_2K$I>#X}o#|9<~n0K>YP znu8)D4;(Z}f0%5Ew|sLE#W57L9-HUgY#-rzqHn>2Zj4v*TAZlRA{GU(R^D?Y)^h@dcu;YJ!AA0OI(6}2XT*7r!;k4mq z__h<&NpG^zbZK^=Nv@>8CO9`mnq_)%{%YO_h`Qzz+;!fe70@8-_8^|8C%aDeKBJ!B z5}qz<-?Vz;8xu%!;yo<$^wG62nI^dXFf);YMldrogDt5rQ1H=-6}mr;z^DqkNi?&J zEWf7WP~ah)lv0|quUA!7ZM(%}yO1CYNC6j$`pmCa*_d|iDuRkEV2mklo%Wf?KtD^XBq4XmLtbn0e6ZK+>o_y-rzBby##G*-t=T5*k$W{Mzg& z03H)Fvw@LOSRQ6*YAnDQS^}G#{eVmT80xukmn5+VNob9uz_UEva%RhxEz_y>Z{M2D zjem=wzo`04Y@n|y(5WaZGZU)afg&*-V)>t*)`} zD-fqZc@^$HFKC>?57jH>!bTdAv~IWr|4J1af?tB$eW zim~6n_v=c-T24(&Y@(&LhoMI=rN1-7=t6Q})D23|T6q3Al-5AB2p8N{Xxl>PI`1Gr z$1VvZ{2LlUm6D7MTbR$y%R@?M{CFi4#i`Bb22e5BclhO#ObLN}M`+S}}}UVzv7**{*gM|8S1gG(gKWBB86E1wE$zw;qba&6iZpVbW z9vyk-)zujy`%=OfXuiU8(uE8vyuIf6n{Zv;n-OEJWG}B@5BVdk12-2}qYxsUXI1LS z{a?RwUcCeNq0U}%QDhJj?Wfc6KFbEbV*wT~n48Nx?`|tKeGV*xugoUybjRmIZEc_P zJbHGg1*J09#)pN;6=qI<;uj#_A@w-I!y^7kkfnxOWYIYWh)xPoa_X;*(B{z)J9X_o zlkiPVwa1c*rHvkQCcnF1X#J#r*%f>A@>7)s%!#{q@7`Sa)jiTn(_uKUPI?ak$kYRz zdOdk~QZH2AR)xDl10G@#-@r!L+S&?8f64Z=HcZ9w{kH@ON<6~CxvIjrU9*4wsGT|E zPib9F38YwyK0BSA4`vfuYcZJd^-OOtFW@q&H0a|X=!N>A3g*MxHEO072^OnIj=821B`1S ziX#Ni7Kbc5O|qTAmafeOaWN( zFCsuk=t<+5L^|8sBO6BMr+#_1Kma>Vxww?}ReZp47`-5MpMHAm@ z|IEK{B{?1Yz`N3GWoZ^>ayoRmW>Pv1mIkItMMWiP1}>;QT`Lvd`iJ<2s&!Vophw@n z{ba?_!R#UUc-~xql5jz3>3DA|Ln9+V!AKEN`!4Op&HAM-BY+!P-&WyuTR=@f*U{8Y zj8D|k(n{{NRj3efuikayj36#xtEKC&1~zf3%O9RQ`E(9TS`BxN!-6{E9&rIqqAA2EnljC+1Z=2UO5AnWjyKGH6>|5T7D@eh z{IAOIA65&8#xD4QNpw4%4sU8Or|N$jv!Z!E)pP8lAtZa(Q@ zwFyJvbYagKq`haqxWWd%s{)=K0_cH?9)jeT>!HRYVG_aQ2b4)5TwI>%Fxea+;XPR% zGIrGeu&6=?f|p9%?kBPl^$1D^D?a1kiB;4Cv7(f;jx*wg*I$=(Uscp*?>geJpCi^J z@H*vINW6vjzr$clxUt{Vy2rOuzPLfD!wKMI|M|HX@WI6<+%?$>GcxK;kkwI@k+}|g zHpWa#+uRcqk?H5;f|gHsC$wWrj+sFe+sIk(slW5UIZ+tff`WT@?i9OEar14pwgo^X znsEcA=&0J+{l=FlSlE^zd)61uv$A7ebPN2;3zgx7fZK9b>N{ql4QGuM7~RB508 zd}%}EY5`1wY)po%c7>*39o4#Ww+Zz42kttBg~$6wIPB5P-Nd5p4tkS`i7D-2g6N(- zeU)_5nOIz3N?JEw?FNh{L#wN~1zbS@68MXH>Ek}jo~U!dD&uCP8V|&Bx{RQC*VNQh z2H+L|ByO9b@d#1NO~7<*c^1sSRc0ZERu5}SpcNcSS{5?0%b(I{uSvz2a6A6PvcCMm zI37(c2IZbF@@Rm0;}K4Qh(gb*eiVIipV1TcTEgYIfA8MF3y!0g5@0b4tbcxp?u)v? zdPnH+(W9M>jR!IQ^k2@4wpo@IkcZH2z zU$%eR z@n8^5(>&(Ac=XFW)=inm;Zt7K=g&r-_ZgX)2gd*keSB(PxKCNMu{c4dZ#T{C2fUaI)IPKmEa`NJKo865AnxFM>oYk3|P9_i0cM`PHITZ+_sHMJ}}(z zxg~RROUvg%Tb`gVVuug!$dI!NIb6P-0h6FpsaV+Ag;LO;P88Q^hFFFxBVA%8Q>G!V z6C3s|xeDCRo@87RSs1EoX6&ELkQGm~6VdVv- z?tqN|N+>|hP{DEcNwac-xElOLJtBfz1gADA)SQ$*-BelWd$Jvsi|sg8^+)crOLf)M z+&R9y3Qs;4Ir-$if`iYvxfO%f^z*A4|5k}oA^!3@y`x++ zRe?Ldw@!h>=gXW)}T9``@f<5BhMV&xe@73M|e51Qg`rI2Bt3vNUtf64b z2{|89n#1EmmTGX4yutcD0=ph4wMm=rI(CI?seaFM{Yt<53bMwG0 z=}Ech%{;VOuvfY%X5E|wzo2jVR-j|AQK)MyS*lbjK>G*E1Z@~FZNxM$x=jZE0y|C>NS~ybx z@D|;B2%arjEo&XwO{%|!Nke#7?ZbhKZer+bXJ-c@q)Hk`Ka#3K?=V+HYTc}uZrV|i zy%#YugHs;89nyIcJTfzfa!Zqg(z)dpe#Sb!Rio+aekPKgTTDq{uSx%q@=NBfGzY4t zsXrVvDQEY`txPc4f_|8wSZte<-UAKKOc6W|s*?LEULk_c%*;${=h0A4(KU z+S^H!(_pOA8#%FRDC=IjAu+!*iEl$~^;^;*UEOb;##?8qwObqwYF{7+V9vsI^%(Vm zv!B?Au}|YU+WA+1siAW2$dMvk%f|Q=kQ)Od1dpP=hUM)%B2qxYx88?DoaZqcGrhqZ zdAhva>5^VR<+Y*;RW+R=W!ts9){?4gCv!^?rq@h<|czznmsch3eq+UzKRebS7pa;>Xqw-HX`gbs3#Z4p;tc#YS}0PzasfsFkJsD32XYcMhx8s zpY(AR@3%FYcux4~M|GrWT%4cm@5nYGh?Y%6E%F?Dgix*M#3y6|Dt`{&FmH7J&V~=n&PJY^fiJWnLgyh=o%DKe1uJB$0kgGkxf{6Yv zLgPsZ{#_3iet$Z|xbA@tzOX>8noNCe-t1WK8C6xU@vk=)Qzq}H!EfXH52?{Tg(i8 zC%zo&siPOUm6BACIQC?w9c)S7v5}V4`b+a0-F74xci7V3t{Zx%f7VL<1@_R@*}1>$ zJJ@vu79QW<(Ow=#9{26mbzt#;Qaq|F)3rEJ7W7!zPg)%mx)tqOD_T-Rj5FwGf->tZ z$1fB)a>NlswviHP%Y!LS6>sk9S^%e4Ff-fhQrx}>+Ogw13bTZn>Z};;yR>z}uod-cbkhWH#wJMV=J;1ca zxE{>}A~C(zi<8*YtJ%oaJA)Ez_T z+dSX>2;V31AFEW_ntBk7G>mxpXn+5{h6jHq`;|4RkJ~>I-=JGcsq?9l)cH$OV-}4D z$Rci9Quyrtw}?N}*q;W3_;`+$&6c>12{G)@JU)T#)Q&~tZ(}>Afu=Ed-O3>)kAee#;ReK|l>#AONhp^+$H;kU!eQO>^Hpf6;U(~KMnCi1BMVxR8tL*$ z0W!mYPq9mRv}Wizf>T4Pvsuhmc0$y}zN_HL+pT*~@hVHYPjZ>ikue{Ch5_S_v=4^d zYL9rsjw3)tfiGr~E0Gyo#1^$bofs~o>obP5|9BrpwT0DfcGQ^PXRN2}`Sa(<$A!O7 zfBI^uErH)#pYiWm{vXS71c+qO=1V)I``qI+u|(+>QceT(IttSpczGX(1huK;$E89?N}J->wEL-ZL(?GfgBI9v>^`N4acLXU#zU64V};WpN80)sA)+_G2|Rw zwfKcMw}68Y$VRmf&Wf)*KzDnNLBEpFfVnci+CEyb7zVza_9MUQHy!$s8 z`_#2)D2)&@Vnn%GC+#bOQw)_yHX41wug#6`_FDQqGE!e(4=7XXcfR4#F$9w6xq$@h z(?rjSs-2$Ip?goG^>=`mzx;sA%iUqKul5C}I+O0nSE}nK{InwWv(`Vw_=19m@^t$+QCF{$0{l`*$({-n@C5YE=gan zl&z8Yj*7#Rvs~_5#T=-SEW6%>1WwXP#>Z$)aRr3tt%~vSaSZ#W@140bW~t~gV9V#S z{^u9FY?BHhEf({GVq*3k&)8PnkZ`Qs!OD8p<_1gKku)22U31C*tZ<_(EvY9rZ$q2- zQ%1y{Vgj<#uibZ1w8d$&1>E=3W_&Mg>v#7p5`bVo1|fJv8;-$}oJ^C)02Pn~9ru`4 zDAx!(>+fWeVjb3SV`vPgHt#r}`39SCN}Yc4W!t@a-`)7$rA7YNZ%P--5ZVgveJ_ix zv)5hbpR1?;L9$16Z9qjQ$s7x|Q7j{75!Oju-Gm5f2m;t6(1t@mK(#H{%8&AiMOy~p zq{y};p{VgzZeFwY#2pY6t0V>H<1X8%7Y0XU^=!>TIZ%ed5xdSDgio#&&DFsiCM8aH zeOkk=dNN-4ixAliLFq?wjbQeCV29q$nGu03pOkPPeuKnO#Mp_D9F|;4THFYwFFQbY|Gf$W^XE$bny8cNKC$u0gM7>%{)l) z4kCu0q%WrJ-MgO%?iVk@i=u}>THg>XfSit+ znOTy`0u(U3Qv3EL%P5XdKyEV5GFsH@ou89q)tU1oHg=a!nd$lSuS!W)=UcC+wr~o1 z=_j2vRn7ddw{Hx;j5Ek5|skZ4T)LWcaN84il{B*5orXEl(qDV<@fXDm^BD zC>}b7dj@r!{04{49NF#;C~{gk&A&}XSce2OYu{>E?S|5rplQ>N+_|RU9%-+XjoF-h zvE&p(nv(bYWU|v!mGR5}EvYb_5m)=E)HORRYXpRfxl+rcwEmb(S(dq+v=7EtLUtvm zq}0BBy|YSQzSt8T)}D$olDF1M^ofjKqYbSMjEIf`sXpPKInS;0A<_oAM7$kuAU@}I z{(M4Vp)-o(5cWjqtq$Z7AR4@*iisVIj+ar!eRQ}Ms)FC}vW3fE84 zYHGv;G|pok(V3+c#RRQsxt{Zr@VL;Qz}LFrd(+u>Qd4uAc_RN%_lv6-AA2u2 z7*B)zqrzjx=0@$;@7kQf&#NWckPwP!PxtOO5<6t`BE>r7&+$y8U)2vobw_u6?s~%w zB9=E!kUV>QOk8zKkKfjdTz!-4l)`9a0`s=NJySJC^vTdI{%s4) z$Us~Yewu=Jc?50_(_J_D$d+1~nT@OR-wbY{*0BH#`pyx|dhCQdI`}Jp(TEpQuw-w~ zxBOU>O!Ux*IdR3p%BtAx{kC$&3&oyFXI9e%UR+J!DUumApJ^&02U zG?BM7GOzT})Y9tzn-VTDS4??Iup=#9$NA?4{H{RP!`rlQ8xHVpaWQFcrVjTfpA8VD z6B0WU1X+^qucx94W)j{bDk{p!nQ*Sm1K%ZdFwr;y+zB*WEL`boH*eldkbsrLb#oc* z9vm4NLD!@wa#WCBvcJqNQ6l=u1MXWzQ?ALnmPzc=b;>8@Z!3wBhQuStF#?(1yUu?e zxx3+4rLwd1t6>BFo`S4ABz_2wAvdWZ{s*vv_=k8J{vqqf$SPCfPsF#!KZK(5|NdkD zk4MQ|Eru!$;9pQsaD)0~=sIxwr$AE5%gfINk$dY{vHCzc08JgZ(~f7xe-WfDvuU#) zap>-Z3JS3WsNAbijd*?{zqjZe^x*~Qd_akW*5-Cicck@NdU}=tqULH>A4mWghqR@^l{<6|klGwD^N=&Z?vy4S9BK~)24a=Vtx?@n2s-(zFhV`I;aX+N8sTii!T_4JGL&mp94Mg(7USh5zB9&zZMx-70L~ zB`UUr7E8=fXE@A=96M&M(S-{lxY2@D+(zJfBWc2n!(=IHIKOh# zC%k&~3Z^?P3RkveXQk_*v==W7nt2W#(%QwnsZWM?`_7$phhb*z#C&i{MKG;LQ{Xu^ zzth`}CyvWs7&DW^dQEfD==5_PXL@pTvu3pS-1rl_vo5lfYdC$c`-u8ZAE8K2N>bP- z>9hROm)U84(mXmcBw+4MU7Y|I*IOHKrY{E^6U)obNvC9HsyxpJOPApc4{5fH)b{5x zJ;m6oP}DM89F8rNM+@jbA_}qIE6Zh>g_pmd64^+>ygo#B1s+`GU0SNW6PK^hxtF{x zNUFP=pgW!6NFQQ#`NK)~r!Qg}keNiDYlbs0KUbaE=H7+`OdvAwLfm6UPa(4Lx-T<2 zKJ^a|+m`Z*h-m&TP+qH0mjI=^?`zK7$*HV^SAHmfNWxjSxr$*sNjIRi9PzrU8oO7v z1t~`4fR|3&k)QO+RiviAUXX|97Xnx0N*}O+9nTlMy-zt(A(2(WMJ3Y1&27Lj^jyyQ z8<~Ybq3E@Q&c=Dl$F&MITT#&62y86r373-X3^Ut0t4%y z%dqz!Ox2-OTnr^29g`0U*txV+S2#oUFU(7{NP+y>QRemcId*b+Y2FHhX)vZsbC%~= zN?MviLqTzIry{$+DatiiBFhg66}R7;0oIDf)~BbZBjJX$fYuRR^0B1wai_%ILBD~K z6G5!UME32IU+UY#y#6}P+VNS-kVE*=#KxZ_P8h}FF=jIy4a@N59pD7cKjPuA%}Xu$;pu-JSZ#yw+!zkGal_5c*Ib0rHL587dW3+i?vvx%fW z;5iR@S(&FVGMkA+qYq%KlehOWf=US~PBo12ii+xhXdJZ_$jh;_FHbRZrJ|9VC(tSI z02a8m_Xlz;(oWja9(iW;xbJh{52a;g)%WBObXvyh_?AvTClGgGW4Qvb;6=J^5E)Un=;!1O8RURX^uI18+roFa}op?k_G zi>?2Oc1>x&EbxP;ETCaU2<_OJ^SZu%q?8$g7RLElGqgaPh9Nb*coCVl(Ooc_wDTZBbcq(z(XWRn1q4kut4AI8+E};Ne1| z!FW2lp4R1wjfmLi+t1l{epAYtb(ELHHsbIcy+(=INa-tMy)WIrzU+}g`eTPPG8cigL_?K+ySz4uIik6%(;&#bGbH!H`yor5FWc+%R&Mudq5>4dny{qtD+_dQ5pcR9pCWZI1K zDG#;M>pzBux+ZA&`S?QbL$PNYzhpgo=#xdrvRRwug$HMc#!@P--6fx1wK!SaBOTYC znw6zV5h*F-lo{!dEw5>6-@gx)hY1d24UIw`E`-_uIx7eg!0gTJwD!K!s)$Kgc2nTr zfmaT)z!cv9Gh~J=yp+duCpHiE7qnzYs8Ny_X2^(m4>l)HQUU8nSZAf#5!-*_iw^wM zm`er2>Hio=_yKn}x0CYn=~0#qk3xlk0Cv%{u}Rsq@_JF4TUve=vazyKkCNkJX)TOK z-`$yYjbN@Yz$?gH9iz6QvO&HCaY*-!-rxhaotmAE3I%NaT;WrZnrd0f%w>xecqr{t zKD-kMQm;4mGtgzJM4q_8$k=m-FXG-uh~-}tW^R1}O+a$Sbnv>2?Cg$FS(gvMD;Z#@ zY;7g-*fte{i#oP=NFI!`#!feoVM)&AOP_-m!oZ;(Vxq~jUEM}LFuFAVPrlg?bQ-e&|Eo2J6cS(y4)j=I{~qOPN7 z-}JA%GtI?d%tb)Je}DA{25z_9ZL2etud*W}A_|cN>v01rc*WO^oIQXiCisZf9+-ig z5tGWG@CxRTo_w|rwn|O=1Ooc#BaY{+tp{gn6Ui0YZ^S%0zB(Vs2|^X3C&rVP-$rO9Y1a zWTg1f_TS&bJOSf4rtGPh$;`e(z2Dce+M#^%+V&N8(px{zV@jckp!XwrZdTD`XGt@W zE0CSMyNlG+DkD$(cMzYdv*-hgq15y_$;x=_QYo$wY?m_;Bea*(ijkv-Xvk=)E9|RNUBZn#GUxWiiE2NZh$~NH5@L`{blDdGi(` zfh~3H5w9iPFI2rB&M_~GM+DjE5b4PSF@h@OkOSLEpB8j7D9EqXSN4-e)fGIm%*6H* z@EQnthbAzfAb&ZmIBK|$M@m9TE1~sSUgyJE3#pNb*~WQd7t=QG;q#UgY7V`VXx@|d|i}i`_pk6y08^Uz+J$<~F2qKjD ziqs!n?c$4rSt|1L=Xv~j)|`C;Y4+!-jF7jh#I40bai*RqiJfGb$i-o^3S45^Lh50*tZq%e_&uo+O<^FVRA`<>&TCNH{hGFL;X}qlb z@#fo0c@_%XFRc@P5?L)u2PhL+>K15fl&YgVdiA)ad(8t)j1}36iXxS-g{=uEV$ju0 ziqKc7Yqaf$=wJRuPxJe@%ys1c=cys49eve4W^z4^T&o8Yf|&uTm0&=nkSA-%nid zR?j*x53M^=B%K@DHcr&2nAa~JL~W>}Pw=^2$0c}Qgh*7Y8)<+?F66 z{V?G-hsHdlteXbJf5#?tJc<8~wSRc!!F0P4oQZB_RI0l1P9PP5VoAeI$4#fGrQWp| zcET8C!L04tUdN=l{GRYU9GUUwjLPq0@eS9iCxH^x;iv)ah9i|>#SAI@sgbyMYOJX^lsbcBtp;b+OyGZc`o~oJnFkXrhpudiUBAsB zH2X2Jb~FQTFRjH*gF=Jqrp5XOc6bGDSF2o$T+fEwZOkeT2F4RcTnMV@IJ29I_)$q| zrVGnBDZVL&#!(O|D<`KYkAW^IJSHnHt`LGNb^H`FNFpmro*F|W&sQ$Xki$DO>Wd); zD@2e;s|^By2+DB^AH5lng~&n9n#*?j`TT8GEIIH8*B)FZ+bV}z(wjM5?#>&UByS(F z0_IN)g8Cywz`XZ<{0ox-{rfRY{P)8U_3sA+;qM3T^S>WRf1me+dxV1j?Q8bE@mlUq zyT8ltZZ6Wf?6y4~E<9!{isl;~L`6j6Vq&W7+lKgVc5~x3C8VVnYAv*sl_&8mnvRA% z?#IGPmu4+m-L_gWFfs4X$~^9lx`?>XV~kBqTr|D+%U~T$>PEXa3GPS*EpEpg?t` zQ^BsKr8V=yDP$}8_H<0f<$SmP{(48)rn|qNFErib{_13i|KWP4jNUF*buROE zImvc!#%{L8Y|-sP1AeB2Cpsw!7jb_wMC}y}*xl#grn5=4m|VN9(Y3QFt%s@Bdtncm z6poQN0{7Dq(Km12RMC#HPd7R+ram*0m5}H)NOx1#(t21=w#duRZ#^00)=%%ey*e>U z@9F7@YmlYim+ghx+MW{KANp4s44F??baZt1uG6X2^#6o(|JS4`{2pl)J<_RMcSqqK zAq1y}1jp5-aJwft)HXEr9VR-Be?Ck7s-x~k<;l*MwNbWJJZoBjg z_Ug!=$o+S-%f;_<9V041f8@}Rknw@Svc`QbE`MH^^G#ks7U`8AUeCb)Ic~_v=> z%jA{$?6$`>9OftTWr%wFqNAeL`xD1nJsvchoqc_M-7c5>{RTfBAfFk_8XFff8$8_K zjS4Kd-)Qe!kh^U;zq+#>U1bEiRvKjnS%$DhVM%EWPQN4&-ru{TFXd91<%j^sE@xf zV}|A}+mK=uqJm}pLBr^wey@otA$P_LtWtjC&>zTmCM}R{B&e!$YLOgbxrvur$-K+C}%-i3z23dot0m7>g4M3JU8%HH&6pqOA!nch`rzi`Wp$ z%fqFI%PxN1YTTm%_C>?B$X@A~Wh9L>_iLk1_-8=1cNkc2Ak>Q!g>6x^E{=|_hd<$& z)0Iv0s;Xiz&$h<8x;b|qH*&oeuhmU&xE@Bp9TXgFlx4FqsO5I{h8~w$7r`wjvDa@b zU*_R_)`I`;bjguWrSA#yXSey=25<` zwgiW?&tPz$d+r@yZKV7A`}gsbSuD21z!Gv=`{cQtZXS)v@Q=JaJ3Fhu0n5pc4SC1H z5}nt2xr|t!N%v#8pDf*Zo95GLR_&$G@p3R@@4(LXwrzZp8?GN@6O5>`uBfCWEGh~Q z3+v?M)N=x{9@IBPB|%gf9UEISZ?xTFzQL_{F0DyX@+;QqWPPM-qDZCoaozlO+c7aw zetv#h$+eIA=EvZ>Pe0i>E0xUFqv7Q29JMQP_WUoL$WdQJU13V)HIT?*2AS+3KS!z!^cJWkAVuD>a7 zSZbkLYm02lYTk0CU*C4NGqpkm2LqsYHn+UI+~s0FMrUuKIoaBLU_0Mo!AaCThQ2^* zoLG;ltm!x?)B7GE9sr3fJD^@Ck7(QL+AGw0cdk|`qqFqO7t##OO%V5VGdsMukUHsp zcJW?hY`(#c#d@%T8&V3@^0+(ePh>F)tF2{s>jI|RJ^!{OyqYb>mq~(G6`*Ty@}~O{ z3jeLFbbM^=NY+NWhvvxtSa>i0o50AJ%DgX|8#+sUG)A>i!2I(UXk=%6j^b@@0>F(F!Y1n%%l^EZhSWv^Vf5og@seimnF0X4z)R=woH0Rlx}6dh*&Kb3oe)SPhgOq{#d^R zo|pgjP_TD~)uoSixIdil`djFV!0llhijtC&2(E-=%guhH!=jtYR?o!w*d;jI&~P$2 zH8ol_bo3m$-l|L+b1Q#(fooxv8{pC>-iq}0_GVd7A$qCKwR7-4+}haLRcGFRDw$6X ztd%)1UrQWbzl?pH8wQ5=vnhyBfd2o<5fT+;RIL&_F9l!!pFti(J2{$eH_8NuZ>ECz>(w#VZ zRn|eU<2#EmQXgzdM7GR)R%TzN7?MKG;ASx>|Ihl6D(YiS0yo)ZKWGrA(W4ub2C=^Q zD%Ox;FwoB*j1-PH7k-rcPc(+Bt5f~CHDKgkJ0jn4&)}^8OLj-G9#8pB)o%}J^7Wu$ z>QcsNqBZ3ss(P3$FU6zoPvtu?&U$*w!+6Vs*yU#lb+_mcguiPaCY}{jOT*kj3DWU+ z&4L+xT3feEbv~_6`=M3vL$k)3Ap0D3=K$)(uAG~CayvTW{u{UJ;5Sz4h$241>}nF| z>?ml2E!z9Wn$DMIk%w6yE7flIS(b=M86FV+Mu!?KbPBnBJmnY6iVL3`7l=CBF}!$79o+#D+f!Zf{~OOiUi@IODCK zj;K(yDJto@w1gbpYPEZ2&iNS1e=kf#`}eTi1L3QA12;qE#ZA}G*gYui@i&CyJuoiz z&{MgB$NO%r2H!HVd}z)*wi@BG$}akC8*m{=EAz^@M$v{=^-HgM4FhvYj7t40BdMoJ zV$Xo$4QC)rVC@KZJ{$|p>=g28IUM}4gtDKaYcyC#Z?sVqvd(vXOX4wc<2ia#YkQtZ zImz3pWZF8tXj-tYoNa+tWN$5-)qipM-hbh}4AY~f0L9vxAo{Jy;HqOxEvb%s zDgets{4YIzF&cXs}w_Z^nV_WP{^qk$CqU~s|b)-f*EV6T~7LI zlkdG#$Oc|1gyu>ZjbjjANjf+>;rMWwR(`AzexpYHkj}HZJ?0^6c6s>*&F14SCz*Yk z>;dOBjU6%Rzm2K{y1upY-huE*8l^nrNrIM4OM!vo6$M^>yI}_#A!@b_);=er=t9Os zDdgzgF_50jU7CvJi7dfddAx903y*6vItnAoUu8zBSc0w(xpk}<%`vC%{AxJ+Q#aJ> zc-(t<-1X4AP3A8|f9;E9=BUp4@nim=I^Xz(J_GwQbB@BV4&kO_0)Jg{q9FI)9{+g% ze__NS@d-R*Ib3A#E`Qf`7HeA5EjC7JA&m~wb@-+;Z!KvQQ9k`D>w%22n``D*enR(T zNly`JjIz7k)wVvt$1Lr=x2a>qRke85;`_acD!=%P!#9ZgN>Ws*8v@yLxJuJ9i^;;_ zc;jm#DoG!se(7A5nZ8+_`=9SR4V;fOtn~Cj^aw9qn`i_oQNGX!84EfjzoohmZ zzc$#u$>&pKub|wdu3#Ie_m8j5gQJ%c!$D0A8b7asJXwDU(2lc*_@uN=X)l{Kdh+mi z=r+8rCo~yeE~(Z}4LP^9!z^np<7ic0SFtj#Kv~n0s5WG4+nDK9#nK{G3VyCNUCOPbmE%oC1;1nPaNln{1~<;{~E`xF_k z#F!!PO_;iSj9N$f>eC!}`3^FJddAF(3TCTKBrAiE-_9S@?|bA}5r#-_z6Yr_bZDj09LCx)ojhUA736i*}lyDEk;*Z>fr&yj7-K`9^)n2r=d<=DAMO%oKb0-j0H+1 zcHQ{HO*g`A!I7pnCVA@`&+qWv)ys;~;@2NDeMW=Nh+<*SKWW8rs`&>~6(35KUx^|k zS^Hsr2EdPGJy$$sQDb=_2Yl z=xGtk^hGLcS)tc_Vx@zF9!z3wJLc^hYR6EWZI+lvqv7TnJe(cKc#R?!PPb7~mNYIq$8j+qy}&L1 zp@8q0NMy~3K_vP=IvKHIEDL?sn$#Cx=_!2MBFt!Zsw_oxAhSO5({Q<)=&HW*-t)Xh zlI_@))3fP9H!aN_9wj@oelW|f=^?_Uy=LR1gSvX%@&|?_jQ(eX7u$U|iPa(O`Oj#A zW4fqm(4Z*F&JV%vL6T`1(DPDgUOqaJW&KD$m{i^>y1Y`{?7X7!i204K( z>?dS??Q0F@>ToQ24E%$J_xt#EQi=Mi$vK5N@x0x641dB;Md~gb3g2j6M=g$;H!GY) zO2(?U_vacd@sV%LG#s90bVLzs8e!`eXWbmVP_I4vBUehwdF6n|Blurt z86mjQ;$Wsnz{J&LvQ`BkH*Q8q7r0!cHA+4@} zPjN1njnbQ^KEt~zjAK>4w+nd_HmpTaM6Rn$5@TB>UE`9XQfQ@ZYMIcAE%|l^LsSR? zuCBolDZKF$IHae;#QyFveGPA?D@6IiozK?71)GwTQqJnUrWDt8RgT6aDqYpY1x2l< zl#?8fbQXqgyY=@X^7_qu;nv&-H`Y0y;66wC3dgxy*}bmzj|z^YuM@6J3+w~`bc!;v ze-(R=iT7%&d(7j{R3nH}(B1<}T*npPs86`Ax8Cn9Uqi5H+-I8bQ?O7uYw)P~(BC3C zPdgxl6gBV^VdlI?dq0{{rvU0*oB<8MMn)1qd}oLXT#GLp{X&K@3OWBmr9b~$`cyqG z^snNypE*&pL)c1&E9cj^u5E}tr9V;1m6~_43P;iNkKPCJO>UQSGPGvt&Ml#nG(3D+Z?=53 zipYxGbNek9hW!4koN3FFxS4vrR_1@^*j4(wj-%EJ4J5o0!UE%wJMAq>!XS0FX8w}` zXa9NU@4_WBJ=s!dLF71}s{ZiVXGr;Wrw%zDUi-q6=M%gwxvXwbL$?|#a&U}Je!KdHqD&k=X6TaC$xyFJ21RSX zzQjnn>t46f%-cTgPZ?ubNz~@;ogm$x=iFf@5ns$WW$}>6Rx~EHtT*=2icWPO|G|>N2R5uu&}Uunz*{j;J7j1N|NeSJU(lv zHF*^s2Kc9qr00$(r*c&`;Cv!Sk@6FIDinA+Xyy}$quXUNOuK01G|?2Ge}~<@jlpPI zJ`jyYMI4!X^E>WYb?-Y{_Vn@ui_lEjk%9X%)#=}oHZ4cdkfJ*r2toz~;YC4-MFgmq z24m7&H+M#oi5APz=zF8Jd0gPIOK$Tg2vSkAltHXGh0^cUeNO(!>EH@2`bJc<`C;}5 z!k4X1iZS!1n6cvu8J%ks=Sai^4Q#n;t*Wq%e>K)mK$tB~;(K7N)rqyy9FlQ5( zc|#xDhL~iPp)ap@nc+OEpL!F-t9I{=GeX>KHu#G5e028y!wLC;)M|6;{Wp60PEbt+ zimTfK#|nYy>giqG+<+pc_HYJt_5G@#vN4`6SgW4X`_U_XmaS}N$NXZwG1Gx;8is}F z>9`QaWqBkl<>pD1_}@NoJw@dkwT0m;&n6Z-WD(z@Xfg3BYGjI=I;q~!5;34@y5ql~ z+joj|9qE5&r1_otyMW=CmMhcP(&;P94v^5iA+ zCg4vs*QR6&u2AqH>b0+ z<`Ecb#FME=jB5-PCj`UYnJY}W zR%do#{{sa5bVxHTZa3e*wCT#B-klvDez!M@AP|1VfAMA~n1^Cv6>CqGZJJq?eQI1I zC&oVII^tnQ1H))qh?gO{^-o9#eQgG_nzyRUzh)gH(kS|&I%bT-TqP*DH)P1aI+=6E z-cZ;TYV~M7r{rzUrd+vtE zDh!Ciq$uz@|GmZHsAA503VG2}44135_-D_a>78K)qCvWI+P!1UOwLGMridjR%$k)G ze`_cSi1dgd77Ab3tSmRTu4F~-G~kS9REHmDt*VUQnC^K&F7#J3{AToXn|c{WZz$7j z8j`XNJM}HA^@D*4?a9{OB`S~mF=%j#O%(B0<^Ch%nFz>AERMJ`;lt~Km0whG@$om8 zmmh0BbVsswH}rsd_3v~JZxzyyy}I9ECo1*k+@07Ye0+eKd;Pi7__&RU&WkzAcA$*f_q$V$7@RT;C8yWJW*rAxUc+ zvci!N^=fbU^xQzd`qAE~hKU8se=$Gwm!YY@v&r7(Z5#g3>{V~O63ZLWOY%U^aZmBO z6GPA_LNE@FgmcXT#}j$<@r_Qn??1^?H@@SDV}%Ti<0VM4Yko5IBb+zt0}FDr74Xi0 ziUFTMH^wGLtNc5(^w)**-c#NAM3JX9iSji1=l}84Od-@y+jqV;od2AtpQJS=$l#v` zv!MFn9)UihRFLj8Rs{NnUFQT{7~n=j+FwS0u@{wA?3ibUjEQ|_0H(2f#2A?Y9VU)} zBgupyc(NHqM1K4tF*U$h^ji#a!)iDPdHuM^#0A}jzjMzNz|o+ZcP? zRgn9VafRp!Cmhef0k>&};D_W!=vva`JJ^GjtH%kJr9vn!8 zS;WG}FS*JlXk_fan|HN8>3YKJ9J2i7sv(p^sI1-oXOi5&uB_DmAWd zbzO#Y?F1fORdxA$13#;D7*1XtCX2e;pC%M#{N)Rv*BFP$@ba%DK)e#wR*htQ0A6u0 zq^^F8X}DYY){EA3bu`cYSv}Ryod<#<^)O!3`afBJHv!6Kq-N3^xKDOW^~fuGZYvAP z)La}?L@K0^D!>MsxgiVcJ?$Sem}$S9uj;8UtHU%k_2m3#Uoenw^J6)G z^oKU>=Ox%de32OEv#{O;AB4qk-nHK=R#N~~4nr(-@b8QcEPwAG_8Qt(Z*Or;pnfa3 z%h{%6y@QfeFZi!W3t!ord2uCvz2#Xu8Dsg~Zhyn1Wp#St^^fKG*ho`l?_we>#RFtD z#S}~(FF)ZE{zK0%>QE6P(GUgL%dDhDGP8>%(`6bNm9nu_&Pk4J*roMObCyaE5i?emoUo%vi;#N+N~KJ6oQiH|Jl^cNWpesbQ9^ zeDKw*sO9Z4qXF5RyM&7OsXd!{`N?4<|8f5rd=rGbi%MUAo@hz+I>uLvz0~^2auoi~fCCY(B4hEOrEXx>M)UgF_5V_A* zGzUlS=Cw6x8ss zZpKpWLkS7O`xxPY@9fP!Z7bGnc=?g3eO3|4xctO&TThc@{rkCJh2_rbBHe5w_6uCy zo*0$crH#*0NZUlILAs0pAMiDRU@l?@=+j-JlewQWjOe`0GZ|incO1C&C{(46ZlMka z2@S~IMucV@H@<9%+j5kYJ>(&ov&?8*Dnlqg3;sX<2EhR(A&p6K1WMB#j4TS?J!8+&o2X+KZ3D; zeqmv6S66;%DRTT%n63%p=6Sc+(t5PUaU04ZG@bmSXK!9h(5^(d)ZsOeLNuf6eg&^^ zG+O#Q<)R-*Y8_`RLKN8K7Y=>yQ+M`E=d?GzBqGNLK+^=gq*pF)_D24-7sp?1Z3^=8 zmS8WZ5=>wU9hToJ5Yeo6Od7>1XYzS&Ew_wrw5~2C7uGJwTd`_w8b>D&)9zr6rb6E5 zgktm8hVVIORwzSS4gJfFxo)Fr+^cwxWl06Vfq3oV@!;nNr~VS=@g(8MtcbGB>%4t@ zq-zEO>;0N{F{|S8^71k=GH!2O!9ywX-W5ecKS=kJ!te=R;N}Q|=2l}P_g!^1ueY+C zmR9)oJD8M@AR7ej5k-A;A|fIzEdPA$$AT{SitVvsJzM=Fucl@ah%GM<4u$z|PicJ^ zeq=c^KBVOExV4U?=CxZ!_Ulxb)Xqei*;;5rw2`cUfBKNX}E>8YRSNZC&nkb3jjOT_=k4XN^E1r`}4 z={@}9VVfI~Cq#0X#ylE7m_3bEb9##H512-t&7c%z$OW8x48ee~zG>;8o`X$7)``~1 zDSBB!@4HpYBjE?Rjx;uRLv)!6daX~1Mq=Jy<4$Uc_hi>WOqNEtZAF@^#VR16nfAKt6ck`6NefF4Ca6|ofx7tX2 zE_!yhZM}3joW{2mn>6RfbJy^rEYSmf|4AMXPSH1A$BTv8r_G7s0uHNQpKcO&^SzSAiT;p9?80K;@_~T4evSb=EPGR9plThigN4=c5}Y{|}v+n-42ewyXiAc;%p8 z=-Tj|2U#`r{$vXOgPG>@VAxba?a~D7@m=bqBAj}?q#hS$#w2%t`vA*p2HOI`eYDv$ zop~#(rJzd8i(;pzIYQ4<%|>CB8)8(zmY*OXBKGz6{<4GlaRmur?sx`UxWpJxEk3i8 za_PCz#akBn>0DXq4ixQzpT-B!O5Iwm%M%X7P!78P#3iZrlN568@cBeP{yik|t;o%Z z&|-4^H$Us0od&C)Z87!Q``h|(HoG#;Y*W1AQTZ$?N<__+e zxs1sKzLMY7FH5WI4A!2nD&a1N)ylxXmZ^rFxtfLcMT z$qUf}6f=xEWKU!;H1K|ohw6QOz1D6{raDaAdrX5uDIB5n*(#9-nlw?uJ>|MZICC1E zgMr%tOK%;*A{hKC^^VKilQlsLX0z{&D}bk)Y;C>&O?}5Dj6nriY2w@jD@41qM{-vx9!eE zM@MHW6m*G>jeUcO>9+$&24Z9zl3YV$++jsKqA`BVd4{MeM|IzyBrI%+D-UeNT%yzCeeBmB3W{Jj)4apuI6U2Vc4JuGK8i7 zvr!zgKZ85aA>U~IH_5_~fYD*O!kgpeuBLlXXqBpnVX~DPA#ND7jpd=@2!~^3W4pe& zaTw>&vQ-CE#t@r@QXfA*kX^2DfhO5#_f=R++n7T!Ru7i12Z9Xa{P;_xEDUQDydU%2 z?I#{faZD0MFZV2!n(lYUba)Q-?sX?bozw^RrB!ji+>$q{Q%&-{l~xIMLFcU0y0h#9 z8c?}tYF}{%Utc(AS9ec2T9J#f7mBg8#UFsUXgSjN-l24y`|2W&HQ@F13nt^2E-%c) zx)*c->*~dFVJd##(VU8QADX7YyiTDosP4iNJJx)dC{?!~W|8xz9{AJ|Y&c65TrYEf zZxdDmO-xKA;NEsIH8r)iW==ENgHL+?A$yZO_gs6H|OpvFKVS8fMh`>CvE z)tb?s6bR+7BoNg{1D*&@_);V8uME5xMb7R1(!s{|tQl7bK5EZvwaeukGFv~H8JbS6 zH@ixkgMmLV?d#H^bzRFP%3jqy%*lV|vj0}*baQ~9xIxq6UC`;$Yo=}m>S4LCR*T(R z)(rsgjBe>ht~gtyb+)fB_QoocBGvN9Un@}foJx6rq=-GZ9F51CLGk2Z}nTBnaH}iHJ6oHbRl1b$#nt}?L4I0k?Z13vR83d?^SzBfCp&! z=ku+$?qQ96IuJKjUha+?app4EqGPrm%zJNnoQbJC8<*+{j3>HD#2w}QHkAUV zovCW{52UK>^ksW7w`-foSksT8XzSdi)ZHMc&E5k$Ee&>!}JK|xvmtfhkf5wWp)eS)-L ziuKjiKWvGjN=mVS1}lI9=6lK}rK6)`X=!P1&&J>z8_7zkQl?ow%~D)iDk8EFhZS;q zYWvT4lbP}Ey?bxpzO|$&7FShGqW1}9Lrx_T!C?sptY1a;XJD{%z+Q!Z{!ILJ{Zr`m zwKG3I|EvX%q!XtI4xn8aMyx2Pss;srW_^v0zLzVGnfi9-3h6^fWTLXvS5y=C~Z-y4+bXOf@%MJ`V)JkbK-8BnZ2-$`K^XD=Ts$--L~fX#7?m$({Jo zzkUh4qM5KywoTv@va_?(?X0e@t}FR7WdiLi(`;5%R?fT7`l_GhMgnI2v&->8F5-E1 z2q)+15I!pEpZ)!Pkjm`r>>M1lS}y&{$|9X-&Cbd)q$%0j+M-bj4hZ;GSon+eIjA0` zz>2G>9q6#8Z|={H;dh_p21 z8zb&-%wTCSbmG?56_vstr6nYS0t0`Rz}t)J>E-6<8#p>TDl22EDm?@25i{X1;&s!Z zF<{ZxlhmljD7bidh%aBh!#8TYzW|UTBqS7p0(v%jy1K-uXh7bdDUXhh!q~qi&(06jE$N$~>pE+b_u z-z+XD=%r#DV1B$qXs#Ts#1p>waTE|94wS-EmWvapn3aPA3JMA%9bNkbK#mOmuV24< zdcrjQdMcXL(2#UX=vMf%Yzh|zG~i&>y(!;Sk%H}*MMX!el&S|heERdwbgr)a?w8Lf zV{S#oYpm&gYFgT0U>v8XK14uN!;}yRMyQXc4gfESgS)+$iIG8XZ*MYEQp?mmjF?AC z%CMlBnHlJPD4bynY{fGmAELakjfGIKvrjF%P?iOShEn{@N?l2Z2U79G{LLO17ziw+ z?%*XSCnp}edDVr5oRZQT92}z~4}Eze&STDP1^g(0(IK2jfNmOC=X_QOp#5v3p}dO9 z#@1HgkFExwn7%$fK;M?-Io<-|_s^d{zkK-ubhQo_XaIgX=)r8f!ZS+&^%3~T)0IVB zf3>=?(Ra8QK>|)c?1PyZJ?}0m+57m;P7wg<7zj@c?q7g2aCCOg>cj_p^|q*p2+u5H zL4Ye?VwgPHDQu}haXde-sfmY)X@sN|^fM`%w>f7&ALoq?5C7?h6*xT;j~qe91)H=y zZ+ZuQsHnP*T2z5;yPW3PlfQiVa+iT8L%1o_{xt^1;|9w0y%>NZ8ZFG>Un!0`c{<@E)t#;sB8^RO{vf-2c6AgsO1Q-(9-q|4WVS~e?9lUZ`{nK zWnx0(-3BQs^jkqe{Yn^j&P7O*UsaHJH087~t&s{+Q**OL{Te2O!iXM3$DnwVWK&G! z$IZ>%80sPh9039Xad;OJ?B2^}^7hWos8_KuF@KB!hIq?rXt;qJK+aB1G!jt0$EC%h zpP!z-dhr5EYs3=KHfVy0AS@!HdWElCsy-cel8=szh}bp{ublnuTYx(&fVTiqSfr<6 zf<{JQFJs+Q-EZgS=h;nve`dsm0J1)~dyt(lV>&uI`ug?jn0SqboXOU3eaR0Xa`$`j zY|{T;XNzbk&IKEI%y<5PTpmKeZGZUZCt&;AXF`Geazao`v}Xm{SXZk;50QX7vxx?Z@6uw}MG=lwDrbDodud4?7P0#?{_Q@tfpWB|%jM z3eFsy6Abs+uL|0tC?vUryaC<;no%SfK!D5UXCO|@zzMd0-_f z?6Bn4*h|>v?=JHfe*gZCmR7Q8F1Mw{9o!Hg$bteby$AP8AdSu=J-P-4vm@_YA8F;5 z_(&BW-d=}mq}8As4wp?OIrVlbNu|t*nK?||8}9?UJHjj(j7ZY_=^mX@#yvGR{}Ni zJML{rnN)m4tlScBscVDydtk*-a)Lw$gEaETX=%~@7f>BK14_Eu*U~efak-h9Uq-To zL3l?{3N9ZYCGoj6ETm}zlwN&L{qm{(le@<2%gdacoQ$uJea!dXH9Yi_L>OK7+r-!qoOTy*)wU_e-zxajJ@6m9_?Ki&=5D@GwEsc2LLQH>u4+ubLM)L%5@YWeF z^7CDUimTz_;Tlmfco5ml;$qaRgdm}YuLpbWFDE{rEtS(jd_xB&ahRn0KT$Ba)2ed? zuAvm@_lCnW25$5^k)H6T0m^08&YAYVIF~e~5G!tyPpu+P$1B4*)Jbp6(H#yzQ!EJF zyX#>l*)c)o98TkPIc?#Jz8el^g+qL1fBn@f7{o+jMbM&U-!8|a-+fswj8>-vX)KIi zOT=tase*oMEXxE@1v@RzsT5xIc4e$-ALCE-7fYmF3HUk#5;xAl@o6y8$?3L#$FLlo zW{?19A{@a75_$Tx$&$wOoE+YBQ}PkCsp9&s;nB6ec#FBZk&yY<%5`Wodp2Y#yt#SH zQ6K!Pr(ku{^T;ikyIV{C?={4c2Ugo`z~PsP!BI1_^XSUcvFpUo!sNT<;|6tZf1l4|=9VK=__S>SvT@835NziB} z3j!`n%frr4JUvH*QDO%NhxuM+pg+^n6~O(5&jl0iq8GbTiBgw&Zi$}qXSF)!HH9SpCI({-$vp_#E_(p(E zs_r@HnuJeEJ5|RFPvwL@yaO}I`Xe)Y9KJR|E$F|uDP~k1U|H+RuvC&teD$z+=4>uZ z&M&RjZL*MFd@VL)IIZU}VD3H^`%});&e6$$NTI*;>lf`0OgBg$x2tehSG~W1P=(-L z*>2XfRBAb)MzzSOdh+yet((G{Yl{a9cWv@pulw}^o%37<1AD`0dlUv?pNF1ND-sIh z!7jLx^7CCL^c?Ry2JULg-=W;@m8)k=jcdXu&C&{N^>N&S9sgH17dCm%#{IWNCZ7`s zPv%V@9v(j>>z|eX3YXcHOSZ@-iZ28UQsk4`!U_E`yShTRD*aL8lB@oQfaJMLB)`!n z);VixK-@2iH+$EU+x>0T5C;F~oX;GGey7jO#nI`JUHbyp2dW5i(^jQ1_s;tC+PR6H z*n@V@FLO48;Rj)(#EnRPTSB<2mQN%BTbVQ9B?RG5r~;L**}5vr06QU1OG)wnfCd3? z)y;^VjK>{+_V``%u!$lA73IFX9|{jBWFE*r1Mr8eSZSL=yO6? zF*J-2h-X~DUmc#{aa)g88#YM{53Ke(?&|Z$`RQ%D?S}uZ7BqghWs$6l!HWgZLA3wh zi~nKnnNUg!f4qN*>iW2q14aevohA&4#!S5$chYX2b<0h724L~G1QQhkSYU-^MfBq= zKNj4JhGI|D^4fEwv5CHIs}qk6qR+jQ5wM7pX`0N_8L>$@M?o^SK(fyGoPb;el|}{Y z&|T7{`g_6}cgkB!ze^emb<=kJ-8IO0Pe1eC_`YQje)xI&)VV5#Xu7Y)k}9SKKWsrr zr5)-$qv4HDjZK*)Hf!+%+@=Fm+uyeqtcIKy)R7js%zOM@u>4EqJgLWTWvH4Aeq# zWl3LaM4XDt8L37;Y^)j_!&~g0;BsQW-LEHIjx$XByv+xWQ~uxUIJY?KCYrXMBpOa@ z)g>E>PnB(=IypV%uA|npNfq9y^AhOh)twPaN z1*pBEF)@B6njVp{>H*jGfYO!1?b+U#mS6=$o)}5aq?or#7C; zQ~va6dU-F;4&2!`ax;|WHj02ffQ58_VeOuD_QK{T+A%~edG|RXpl@~A)&0A}@^^VN z&dsou!b2nwuNmORoV;l4UWTvQkWuU2vt#)GSbNK`F1zM!6bn?OOF%;DmTr)eM!G|g z?(R}Lq`SMj8$r6eySowTcm2TUzn^y>dw+UgKitQSUR>8&vu4fAnmOlO_hZJo@##BuuWeL^EBo76(yNT7A3|0=_&5*(?2(GpZw3RCHti zGH-|r>R|}jhpU0Fgxm3D&?Rq%Q_YbX>@L!R8F@34m64j>*3W1ELtQJP%a{&fYtE~) zm{!fhl6zC{`{wg{duI%EbkJ-(c8*n8m=5S6La_(p7%7;Ut8;U^W#P&a!)~_ij(T?^ zvvldJ-yu!ftSmh?L6N>xEbGi z<8;3MgF16FB?}rfIZ>oA*nd1JB$IhW99M#Vo9)%O`8n+$6WOd*3fSyL2MQiQU)Z<+ zKnI$S?c@xPC62uj8_s%@P6acbVw)pagwNquZ86I%!31a4Oo(gu8I8pK5IDF@+a0I5 zI1~=0Oco1t5kbmBmz(^9sMw<^;NV?QPylEFoE#kB@VFAn1_4J#oW){5Kme#Z?`>=_ zX|=t?!%GHnHHaiY$&g7d?8`LAdu%5%gH}r1*_{3LKU8ShqHtjsT03L806xs;{l-9! zHH5eVZQfV&w>Kp0X!U#r^2{gdQ!<6G)z}=d9avP1#a4}T7k{DeM6**XDs&o!6rMk& zU3b<;IJ>t^rwI7*#;$hDwbVM?eyw0f*l9!NTca4mm#-}@dmrNAkME(jCp`o!8GVg) z_pGC0MDI;6RYv1~ok`l9N^13va=6rZrqj(zuB3&JQu_}O4g-GKK>bauZ9bK z3R%#3G_u8cOBsx597t+%oOC z4aDP4G8I%+olEFPm=%Y}Ecm!Tugb^%n008^KQk!bPAYzvg>7lr5Ga}$E1spUC0V_jm=hAo0)vO7ledbYXB3?C{(g$+WK#5_D&cOFWtl89;jK9d5D zw6wJKAzlBN8a)Y#&xz&yrAxF~MXXBjLBnl?c2}? zUc`W5#Avk<@S{{8II~5$%)wH@g=JRFb(wkGIl}bg^ni|>If`)Ey5f5C4wb13s5C?5+QG9SBQiJy){s`O%U2PTS83EmSN%x1@#-KfOp@O%z}g@hp2 zJIfzt)5Q{4LQlS0VwBIOcUAF3%)?_b_=@Q-Hn~t$z?HSwOGLY^1%5Pt_2vG6eqnzA zr9)&7uvMUtw?|Y#YQf;JCZpZOfs*F*9`;hm9gtR}SB#d$E2DwBu0aZWbk266-OvcW zqd2#f-1)Fk*ir06>xbTP3+ul0abTeYId1!FxDlepsPJ;a^?6xnFU16hXkmEGafwP_X zOOo@+nsB(I|FkYgHh_yye(X76&N^;ucl<{5Cu#z}--*^}g3>{lo{p}s9^?D>?^%CP z%UhgM6+F6rnZ_1#XAh6y_h+U`B|I7B1VXk1LOy*Ea>t$Y>tme9fvGh-K$u2yto*{letmX}&Zpvy6kC({vw@q0r{^_d z>Q)^q=puWw;vNxB%d4y378+Atu@}h==kr8NBWey8PT*gudm69m=!dQYUEhNng*ZS~ zyh6?{20O-h6e7pzl*Nb>nUQ+8{(fN*^tn!KWVIrh=-r{$>aIJt#oWIrx}Xg|H!=Dl zIdJeSL|HG-7`&6Z7t{CgyxC;7R-G?~hT}Cr40|RRS_A-rU0vWT$5a;7lBC7-b})Af zgX=X;fc%tAofopxs=!u1gHSq4<9wP>wLQ&VuDpP#hx&D_T$n9&`G~wh$c47N;%We; zLg#Qxu)NCd3#~2Zvn(r(+}FxmgbrrS6=~7kAf1qla9O;)X}qR#{|67cI!yo|;d?D0 zkrH7bneOay!oiH=eq(w4j-PQu+ky~M-Ntf-|EP(%7_CY@M#Y#73ycAWy`Rx;N58&2 z)^Hm1@GPsERnK1olq>aoJW~gVML5zI_yHW?k%|{|Y!*)y=HT{JvVGRb-lV->W9SF# z{@(^@*|RCo(=&}JnR)gT)EsoEI3Lc-O3BgsS*=5bQ@3u`H>wLIlM6!y(DPP*rK3S$ znXP7MSlVUK(^4EH1Yp1W5B)^Vo{fWM6*H#l$6}tQn~C^;Jv=4?XL+T~hqVUPEyO5q z1npSP1IK%a(zzS|6twIq89X~D-L=vRnWIg(vO6e^q zjDo7*z|%Y)l6n5KAVDyMuQH5J5nQ$Z9LuxSd3AkFeiVQ137>}~mgfin;4lAqcnnK`(rrVcOSQl{=tu{I{v|rCxjp2 z$IBLp>dj0{qo3?5hqiDi>)NG?e=7LLa?UW}E`lR7FzwX{kmVb3yFQ%lTo`gInYziS z3%}e_@r;M5UYbrASZ6MSE6d8&zKg8}eT(Cr4o0#bn2o0PCHkX+jT7;GxF*=aW{?+Q zdCvMk#)f%=fFF;^D#aL9ou{kceBuADkOg#MAS2I4jt(koj={LD@VL{_rnA z#OU+{0?7FO=caIkz<7ZC&nwobb1yUdPa>>ypjE0`*dwAPO&srhMv~5~!bCsB!yp7L zoewlFZmK_kPg8JQ^FhmMOa#b_*82hhbYe@+tKKD_q)pfGV0r!%ZO~<5kMxJkW%#gD{h_q$_7U-w(DFRxUixILKpGwW zjUU%6i}TjL&y4G=kpWH;ZPnq@f()lH)_M4=bxy42T#l&3pkg0M$qi3Z(+$e(FTO!V zRyJ7}BgBIj56vWO8`W`YL{HKEJfe?g%@?Y8NVt~X1zC#l9cP{ma9qF8tM0{nas5{FSbw$7@cD3+k85 z^eI=Fq_o>2#T_x-CN45$y>73B_tUF^*1P4|XPo6`lNB^XJn%}q33BIy7%Lln$4ia5 z1)1%-Lv4#?HffKr0W10H)tWpn_V+5`Zy`gbul5#wkwrCjId3g>65ad z_D6TgSNB#OG3fmAf2THmgRrtte>!d0BTbW*V+gw!}^FNdC;+Mo%)A}0Z=;jN0b$2)4LXR zV#hd?<32u@Qz zb-PB`rA-**#VE%V1N=k+|&K*9JQJ#Y?Ki5z=W>%pSK!I+P6$B!{=GwWv_X= z-qo!2FmpY>vMjUINlUpehf<*Gr1=?5$5@XCajWGIuuEg*t7yeMPi)ex>cH{~2hO;y_cSJRpu zLJxx}&DI#LEkg)CY?x7B`lFpKk&IHARG)Ik^nECI6=LI0ruVC%rhwLj<^;5_R2lr%@6VAS%#{xBebUy)Fl_+tFB%+*`^jgYU`tvllz? zWJcl2z6%|fE@pE3a#t!1q~@%`4w5`SLaUJ}iH- zbxCN>f3UY(ue3am=-^bV!x~FLZ!S))0ocUpJyO*l>#Z8K?VJ=O_%ywUX6!GMeIbql zEWi0{IBC?>E$##dif6Ad{TFn5Bu_u{R&R>~Nbl_&phc3oVE>h4i4u-HB?y~rHY`lZz*nfs{<`0$&agsg(?&#kqN~A!eT4O&d_-|dm zP}fX5g8pD~RY-mv$iMu6#-SPyqM5XX8~F)K5-(xKnWfHV=x>g*@ER^Gnn(-dzh0yH zU$2qGf*@rXzP0Emls@Z!Gis$;$ZEX-QrApkadO7!!}$GR=B|CagTb<$3VU+`^RK2+ z;nFCCqXmNc*9x$#)jr3P<=eQ_wUu@5Tc;LFz)avz#aw<>6M!||e#eR7E9ErxOW3BT}yR$pl-E1573xM=7ojT1q9%mxLO&LF! zzbbiwAP$r1awQgHf6)OWKG(*E6%mp!^Zuf>jqtGjO6}VClF$Z4O^)LakZ(*1&m&iX z&cX#TR=`45rSOImhlT;a*beV$);0WZPEcR3F?23Yy5-eby~e6hX&PkU zf}taimf)DmH~^t{@y3*x_eyKy84(PUxrJ0^66l%{R*<$DZg)gNE%LXXEcE%?@4D#_ zU}9(($83hXo@>^8TXoC`$~gv=nDn{;f2e%)91wo;)#I>PZxvQA#Kp&pZhDaL1sKm( z{iFtT&kOIg-Rf}o0+I(rhb-ZJ<2K5)* zK^Q&&^$UE*8b+<4uTNS84L%GyG}%RrkZS~N&tyIr(^v1=;uIMw{$zry;?dMx&JYgI z_%Y{tz2Bh#@LHhXIfc>1#RXuzC~e^3;NU>L-*)^fpw9#u5d=<;n8+%x`9>D*{s6T|S-~tdmkpQHvlqxAD#S=tIGXSVp>FMb$!~v@1A-pO~&&@Ts zySW6ZNPkG(47A_BRcV&!E3lvMWyTiJ}Ny4FT?=G5|D_k8MYO}MmeSA6r zdyR&NXBHa%{uYZ?>$%PjgPNq|(&3@o`Hmb1o^`GBC7l{k-QTqoYjz>c z?I0lhrJe_QfJj~pfaV36jvY?>nL$&<(`U~B1j8F1$B^HMj*hM?96(HU`{U#Q#!FU~ zu-A-oT*=__WL?^0ZTkfbO!|0$>60W%7&ezHhWUDsd9@U*Bio{Z2K!UpZy~vk@JTm3 z@;sT(WpRFPZfXGo8=JDNIJf1R5a3?zM(j^uw<}651obyQ3IHiAP^)L+=GGWctv1E! zuMi?g1t1~;4_^Q&0AR!3KYi79TRo#h
^fcWAWtlGTNf#w!KT2N48B$yAP$F5zcN%rLPA1R^ta9v$kX~1%Dm~-j-p1oN=lemG8~ZD z2kDz=0eHE|yWnsJA*&3>uQ*#p?QLyKY!CB@`SSJaSAfLB7U;2s=Bpg7TF?cU(WHn7 zPq(iimVttz&n+x8af*}&n9&KAO`I+u$3$)SD^Jho4584E-!3aF16-t4E+kc>^3`>9 zGXQml35^5R(GqzXf`W!BQHzUuv3+_KokN66~hN$`2c&=ZWj;umZPF{ zuQie-=s*qqN8?(-;7p|vl}Zc@L^YNuRT~{EIq8OhVgTlh`kJR8OKq!Z><#+1D9FfK z+~^=O0=P0jC40?z2_WKc-oZjN5d47*6#RjF5B!1XnY-}UPYjQ083@SFpFe+|a)|aI z6b}?^?AanBJkas?XA1>rHGo>dBYuT|Hld)lKzn5e5P{mYKOU^b7kpFe?{0a#C4M?bLlyHU4QruHziRG;1VJxH;TfRr=`R5}`ik&%!zCkfQi zFfe9Mwl!7dAj;`34?&n79GH4p1_2x$KxZ%gs7}^FM@9Xo-4#DhPAqpTX2>Wg$`?0y zsu2DypFSgi;e$!avq*x52rv_Bdv>I^Z}qR|ryq08V;TIe{*wyvxpACov1{;`xUA*d&k`lhN-tW^j76g6B z54IESISh=7=2r(bLqkL0mMm_mg!J|G0XDV>kobW;COkGd2{<_z7#VFMzN{r^ZYr!GX&%f<(?Z69bK$iTb3S_1ez)m6Zdl_qXG#(egxkdG86=u=r(t)2U-2$ z0U{Md<2OLMRkU`>U@SG-@a{LsPTb8}YHyKek9r{kI%Qm8AD+eOD0AtBcd>ez%lE+i z1k@Bu`(vg{A^`qAJRp(5BaBBZ9-QDBel4C@XN|SRX}&H)abEOiDAETy7|;3W)I*oj z3liEt5IstG*xox_z%eg=)E$VKN}FnQo5n7q;(u9saK<*zxVqr4Qs(BDdiz%6H6(Wj zY0{aFg3cVzggeEaaO1xxX>#O^WgRkF@*H}_P`WV{x2S!;u~6WoH^)w}zsqnbWmxjb2IRf9Fq+R4l1 zG~2;^hr?li^L6HC;=J0BI@7Po;|d;&U0>ucs&d`>IF(V73JNkt*k9L`t=Z4qck8QQ zQb2Spu!Nt$zLLjPRHqRT5CELc5=A_O48L!Eik&RxfAX0Hx=`8v>6j*D$koU&ELvsi zFgZEthAF}|-(EwmcrP>%ftPNXUe#Fplep^MmA&72C7+~d*LFUSAU`70awV$X%wgp| ziRJGl4+P6+?Q*#Ljk?`_euq)7_Z=iO1Y6t-c!i~GOi7DE2W3FG@fr(aD8h2WmQ5!XD=U5r3Cd$yXa-|#1nk8;Zy{z$YsR;=aY3)lHgt8X0w`g^#7CFwjPkLt%33pg?-Z;>f`l5|`POW@)#QLjCqhsyYWfb% zMxo(J!8z?+;@{!wpSWB)f79jz=A7ryxf&#`09f)jF@}f^H ze57Kmf|Q=IK_FMjU_!JJHBuncFYFk7(|+Li-Qp?t{bG&1&;EkDJs;EcwZp`3`f}a= zzBo)B7sYbjTVSVgK(`bKJRD^ssa0#t*L%?Phte|}L>(MIJ08f);ke)sDTf*CX1$Mr z9Fw4+Bw^vm8`bO1wnn8*a3l(3tlp@@TWfSmF-jzujOTC9m=#Oxo~*aYb@=h8g&7f*Vbh(Y#uCvx2|-X{ zkm=L~gWfiPqfAo$-PhNbmq%_hiocG3+AY_e!IMGv>Ka){19hdlTyAhXX-^!pmhF(FmQaEN6u~-^W_x%Trx7z&{?-IQl#dqH? zucrc%x#v(@T))jNEe%ag50=kCSIR=AQFNU_2yci3O8ms}@QLvI2Imc!SQnT-Xhyzd z4*kdEwaR5Tx9dQ!DfKyOQ8C9$jx-C7wx)*byz6$GHNNz6k~DBU8mM`G&~6(YtuR=; zx%}IQf}DIz+yj>?v&<-dlL*S8N!d^E~{-PXtaM7Rk8D6$9JgE8qvr!=Br-#Vs=o8$`S#Q?f z>2zjuB)M6d*i6O7F5{p%G%r{rh`O2akw>c6TCPtUN>0>L!ZO1i;C?98ABy4es`P4| zn8GPlc2qXb&#Kx8vy0{kLS*6I6ze?9aKF`e615ZcB7G;D4+n=0HXy_l;~=AD^CNLvGtlH!}qXg^K^ObLk3P7FzOI@qxv{)T5Jkt)asw z!Y^doxA4R#q|m-OIZi|^IW42NV~ie*4dMF!#`yP_`9dCAnvN3T{D14U4dQ3e$RWR; zr>3ECxU=JUaWJo!$@4oK4#SV%*m84XNdu@bdy>k@JG|R76RxZ6289h9uEu2oGdb!} zG`O^r#CLzb>+R{=d5Hz*;_43Qs?AlO$^gb{;L47yrz?#VK(`+G1P24dS*sW9AiqZ! z6~9)f$2F(6(Z>N#Ik@1Z~`cN^SLd%b!(9XW8j|7Aio;NgQ zl+s&2KFkZxCBMDpWsvi>9FfRSa!|yK!;~IK;|pM$R#+Z;)N+nz;D86LnQZAse}6wf z&w*=nwq4=;7ty}Z(OzWMzP?PoOpQo(rAu_-*`RCjlCM0k#Mo2%^}68N6({#sNr|RO zqsRxOY45Ml+Cl+~%3{5QUn+_bspN)IRu28QUuMB)^?dWF`6KxjJHn1En66vO^S9x) zp-y5yGBn5V1nb~n>iRi%LA1VzZnthJJ#-dy86CPQdQel4aV%j+OK0ktqk$kz`+tqt znt2Ves;Y{Lii%wM9^4lK27fS_GWI(+1GZ;bsFMco%O&uIRBs&orqR<)QpAh+HrjlA zx$7cc1M%=pc?I&AV_i}w`*V}$WE?#eovK*tLheBjdnG%O@?GLtCbx(eYREf)#oqG{ z%;=|S8DyM_q)YEfj~FD4c^wN!B^E6)IW{pjRPsHs^zc_xg&(q#8uzSg!BIp5Sw;8I z+()f%o*&8F8=!5a61V)M&D3CHS+a&4!>Oo zjpm}lAE(7SRB*&Ffq`dsVz|$MZ8MSQ0u7o<^RCs=vZf6*WfngG}~)5v?doG$!v0E6TWHv zB{YWnM^q8|wRB>Ya*fhlHV?%CU@lh55ES_N!62P~h0gj!()DY&aa613><0H7I^PRm zmOS4f#|@XJ8`$-L4Wf8~W-#b;tMg5o#e%ecmnG88fjoV<5_A5Ps2WmDHHMcoT< z-8K|sO^TxFKL2W9GOv6|(={`XQT~`5$*BT_s9A3zvCpsa3~-W9O@+`!KOA(okmrvo z9jZeU6R9FaAP5vlOzBRo3F0u&P{&@h4zkk#r1^z!??0aP43aXFqb+eJ@$~V*zg(P*0Mr6y-(z4WUG4J(iFkGcQ=C3|@Mp0BranzqsOWdNQE zc=4qts!h%g-C;yE2oQwFV_?k*ccQV^d{pTbY~?w!o5_l|iRvn+=$a2Yh#w>A&mIgF zj6b5FIcJ)bHp>cpRLYNvJQewjcfR&8O$S$D_z6TXR5AjE03l?lbG5K3uxjhgizH&9 z@dUY8J;!6IRXwtlB^fYRwXmUPMPL{c|KD5OJ$)=-zmLLIt{hUTI-!l0?9%H0bc z2ad*=hN2jIrVQJ%Uiw9?Z8su4@Cidfal8gGEkx0@KfBv9`ZDm!2$mnZHxZH+sOVVl z=}U)TV(WTpd-=Adb(IPAoV`4KyTtOT34!<{QAkv8Tu+x%T2H$EG|7UV>PU^36GoV} z4K^yxvh*xM_j40Rg4r#vWClq8fmN6K^kTb#D#_Gl@*Z7N}V}r%Z;sL#ULWd$0HCldfxP*erLUcbyzInYXXBH-hryz?dagos->8~4K7q|&wYIb>>DYMoT??t1 zB544*o}dZ^Ze%^hciN zdYt)>*?liAso(Q9J`lCKMZQt>P|N+e`$3vZV8)MOhK-Lceg6H3%t_XeJP7H0eXI3|gqw%$W5hca~)>1CZzi`xfZpGiwqH7h@63-+gqRIkFI5og=PAb5js&U$N`N zj>GiE@G6(U`QN;$KH_LJI7s8TkTKnlUq&576<{U$3baJOknJ!EntZ0=-%p+>KqOqU zR!z-W7h$6$MSDSZ5c9bI^7y5shQqPg=T1TH43r}Gq2X4VW0C!4a7b61r!)qI2B&&h z@_t-kTVYsHAW+cx-Sl3z>c_`meET~owunAVhb^1o><x&%{F&`3nAK zPT`?}E$C0Q0>^E#b(CHXq89iHvxD}z0mhuPTCJfe9%S+E9t4FRw?JKOO@33sNp`Rk zA7i^57naq}{~VQ^t*EC`y-kgUhTqDaqVG%5mkTkAjT?Y-p~Z#t)4V zY~dFg=tDej#Nb5SD0H&n2VTk-abU-0n5BJ*r2MPGbD&pSUfoyqr$YWt+W34kpRtm$ z-#ez;w99jTJHy%#7$G|PN7EzgU2k^?s4|$CnOEgGnxLnF6Wt6!2@Jxanm`QyAjCk# z_tfX*6AEB`3w)|YI?_BE(7d$0Ua|-IV+lA(eTc+lb09TIB#IwDAREaWy^8sh`jpMZ zdc!!8@5cvyi0k}7%S}|_6)(`*U|Dr0hO$;{`-kB`m)!W;~7NS_ARhO?w0P^^c8s%$*C@KaY$j}JgJA09Lmw6ZD( zt_^TX;@Nh#!TIIIXHg|(jhsJb6QDRSPfkuiga6p3Bj9W6@9V>Dxp}A*IAR0J0}$>@ zZL)t-URqnr;8HHJTo)NhL8Fjg!sCVAYk`xu~ zWnOefseS=5pF3T^lP4x5AS$Z6yk#~8N&Uq1^wkJ5nXT-?LMq;!r{DztNjx_{pN}Gk zYiweo%uW-GGA&teFi9sL-1XVk4=I?s&sbdlF)_rlz#DrGh|D)WRfxf!g~x_8HgcN6 zKLi6?l_g{-6ciNGs<<`j=BhGx9drhYTB>4zWQy)l>^eElf2{G7tV^V8Gc zE?e(LKqZOaI{u+pb4E@Mibv(;7T|JkpepV&Jn-ORO)v0E58r?X2n{i`XOBMwfrSPG zQ)d#TvnM@j;dpo=0(9nl78G3N%At?pLV~FN4lGxs+jDqajwz!iEZ}${i4u1oe85hY zlLJDn$4LeO{uL6E>;>8i5Hdad6>boq3j)jX>* z{0`A2q8m%G@X_4%I})6f({II{pD8u(_4dWmqzaqLw1`p$Sh2r|hr zjNbhFcq6DVeOkR>@C^v)9T<==Wczl-|FBd?&w&}Ww6Ouq4wR1}BvcL#D9C}L{~F)F z{LD`3av_81>@%8_R2yCCTNAW=orMED9JdC$k_wdjDEST%V zY#Q|q3`#32+uQsef8GbG1|fKH0Y8HG8v2M4Q-Dgj%a$%6d63g5eh(R;`+~5wwKV{~ z9PlxRM@M6zp=I7smz9{8R{WD4 z;7nLrUjBZ4BnH0z>dhP3Eq^EC)run3+7HcfQBf1!@(*88Zw2NCq>Tcuj|y_~^9kT0 zFW%78mv3xrm=<+_Ijrl|L?v?_gX|9o;O(U!4Am0oaXMNCGz|_fw(l;qz(EuM#M$l0 z^@qR}2KtzXVgM-xun41RnVACW5CWOSTN`r=i&PmrZdcWz6u>?*^aBt%;y#AyTku^R z1%-u&bG4)Wn~nCzY15oto?9fWA+@!vR3L%=1mVF@KylC>EHr`(qNhO_M`vCW=VSJl zKC4uHiHHd0p=etmF!Lwwi*J!$jy@KQ2|i9mk0J(tK>|Zn4qzB_JqRzk`~~)N5}Kz;JPP24}7< z%-Hz&UhT4j;%5fODLfuIPi<{&CAA?7cgB?Cb$W7=<|UIY(fvhnZfvZ=Y>`WUh#pWl z8?JSQYQF~viouMh0620MtPSYm?9m~bxn%;(N=b1FFeX4M)QczYb$KzA)XLbJ3GcTv z=iq8(2(nJ7%WT(WIHl^zOl+_c9hDBs4!CXXvijqhS2ZA&{uJ9iuvV?DAMo+NuC)09 zBi!J8d6FZQo-@t&%d?2yXug2ze(I!Rv?H0RC|_d|jzn6bcTwd|Y+yDK!0sWQh5+oe z2|zF35$NaZDgcLiV`4`~7ibF~D{NG5uEn|pg47AtKB z$>ndc&cwn%SU%{S>{@l(hP49HJ?R?0rqzlav2R4}dcj#QxCHI-yolQmxM#c&%ID|j zX$ghI8FG&DTe-4aREcY}C$x)huV+FE5QA33;0ej1`?!)hU2e~Uf<`apA3e4s186<_ z2G0P%Zs~%-E8aOC&~gk4Q}#1w)rT6c`Kn#-SnQWT(!1g#2-QHf0DVtwss2~>O7QEd zoDm8YRsyYdhfLhhj+TfGrFK}*)V2pf>1BOO(3kn#RneA+B)G7_p z4nEUNq$rgsEek9w>6OoA*U-4Rv36QWp-G**0D*5UbUhv32>nDko(VUZ9FHmi6@9-@wB71v zBhc3fNg-Sy)c{dl77^{TpPBLFtc8n;yjT&9=kksr|f#@LZL8+mL!(~z<kVCKKI!h!bZE(3wBax+NnI0L=8zPW@*XS zN=4suTUsa4r3Z3yq;6r(qiI%e63Sh+Ynk}KAzS;}(^T>LY$hj6n@}Q%`yaQhE2t?CsdRR_KnED=57Y`BPd}7151`v=F z?up_nh0cdiP1U=v)_~K4zRYxg8JUAj8?psB`zYgJvW^h+@l8o7;;@?;FFtY6Gmhcu zOl=Kv4LVU-zesxu0_YH%Py?doL@{{OFti`<9||E1Tie-j_g+vu9ucEi8q9?^=0sGK zE@ouc8%DfW&r7`(2Zg?X{JeFw7<=f%Rb!0IZit@OIH*ky@C?|d?`5G&FxoL9sNs+| z%em5k+y#ofSO|!9FIjvfD$cf~oyhP-zw~l6>-T{KWPbR*xeD zn=^LSA(q`bGG5`=ak|{1QTAF(WKVpk>S-d#&Zg;Ci*`wI_wW zN4!E;<2m*wT5IB+nSMx}0U~ccB*q0Pq|lAx^|=m9>>$f$CbiLAn`v|osjN1eO>}?w zYG0Mk9lNdaYf;mIn>!1ml9tqS*D-8e`t@9ozDS7r^v=&#MN)~y4JOw12BT;lf(v-h z;A%}USO)Q_bcW{P8xKsEHcTzQIFI(={39QqZw4|S3+2B7nLN{5Ain|I$7P!0MVS+r{k82G5>S=`pxdxcUsfO6pZ&TTsv)<8N}?`rB-g zyY3tu8bVVLl-MJlFd0{COp%>Fh=7e;)pJ;RqH||mQF47a2W8~?a@hB{@ONMePhK$( zs9}H{T&|DI?||Q+fPv%u?m?BneI$C>!l#$1{kx0nI_qn0$G?*=YoML}if7-)tJDOc zh}QIHUycJQ*7#Q8c#Y=@C-`AjTf@4&Bj?~aYAQT&lKL*y(f~G8x5SBy(CO=s{B@K1 zA%Zb50huBofA3gw|8@Q7#2DG|Uf>SYo{UXXe{!28M9`Dn%CgOdQX0x{7yO++E@jv9 zA{eBMB@+WIHIPze3eOj69J6Ke<|PbrF5}w`3SBgAHA%d05_|!8Ck^|&M#3u56YmFK z=NG$l23yXf^`^A6QT|rsgIaE-Jo6F}ZxH;h$Rp-3=3)Er((h9m&fZp%j>tux8T(NBkBd~3l=}dHizgF#Wgr``8y|vt8Qyfg2sv+2J)f7B0Toq zG>-slI2%v}ktO<9nq?4bAML(h5npd~vp&ams*=2cU&KB$#L*3vhxxx+b(bUkpD&t> z7%d*(er_7A(R|{3C?!(d=&Ucf$+HojA13S}q=Wp2D5QzBEnF`@X_Rw1o=)B4E@xBf_K4XT#?Y?S8tX*!zYCulROQsWxV{a7zQ(Itv%t-D}z{vlNpQsx$1! zdv%Ghv$?R%EfQ?B4BmhBaY!3m%0j{aNv&Nb_2}fm^+GN+k))Z#_rgCVW-44wUk?XB zhCT$kYxq4-bCfxWTa~k{)CR?s^T0F3A$v|bC-Ib{>R+cadvSO!dI*l{2FcD z7SxRqr`SHXhuoi=B12zW1kh2$9tM5p7 zP3q&V;5OyQyviqg#mBcu9uY?Gx3U;tyF|$kkVHPg9r;Lo7osM^GBTMb8Cp z(UTZn_9w=SAM?&3(2x6GgYBbq->ID#$)>e5@xuJ#iR1iPu4O0pcRp5lH!^6D+nWE4 z&1?*UBJq`|i>L|c1MQP=iADhS-pRqW|+4^r`j&fs@5j!AeZ{XaUwMMI@t^chldIK1bkyD~>hDZ&SoAq1RVDK%?FYW&BcobGAbN~$sDJ(t9|GU;`9oxG-QCic#)W%3Q+K%g zb&d@ztI;zP5d2by5jVM&zUP6XBC7%cI@_{2$Q0(iuf|o1T*&0gHkv5plzWSLV7vaT>9^L_5yc zt#U4J@C_`7{n|F_J4}!)G z^$AS`B?u{DK6<{(_v{%we-{2!L=LoCqBqyZV6rJA3Gs#csB$;E+)t364KKiR!2@R!3tgK3QUyE9T&$V@PO|;n|FAtr zE8>u;yKrmv!+ZLD{y0#I#Z(04pa6I(>84LT$9@->(F~@`3tma|9NRCAn$$-Rf^gKz zCXH!I`NdQ)z=3aTv+Kf%YW=!#Yhu_Eq>l^U)+z{oD+h52M4N%jAj#r6u3yQ05;Tu= zt&f$FnICDxB>FayPi2#X+b3%BzC=BZEH$is5iK0%c^33*L8~(x;|l^t00RrdRvusp z=jnX^O>zj|lCekQa<~T^$9B)g{m6rn8GO~+!Fh=M9TP)xvJv^McHPOOHZ_W>K8oMu z4^CJ%$LdV;?ggv!WSJhjQ|C&?TOyXrGTB|I`J z^Z?HGP_lK0xQ`;}%!Y7YIP+$x!BaM;7ecCTT7ZyA%67nP-rsM6oA&y?tMgS$SFE>22ne7*E_qmWmTyZrWUp^ z_=ferjCZxY&5@V=RPbINclr?%2DekgSlHCRf9219X2jf&`IjYIr7BW%dJcsmD+){Y zNaxAoIbG+jpFRX=p(L=Z-~|qu1iC?p&%L7b%wqeIF@sN(yr07_M<8D1#6Ta@lyek{ z+r>`Kq2&Uvf?N&b>F7CL8+zcPHrKoc1Z2k@SL5La@1VduU|2huglu z+hL4Z^7mZFj*u{cw>AX?{r zOnHE7AhA9{Bf!>oK_NsA4YEEtADyYIG*(UlR$h~A*BE^pwl?$!v=@LZc48}Fk`(_3 zE@JlAfNz$PKl)P17obAf?*AA*LPj(>YMk{VTVD|WuhQN+tg5wZ7iSx&=n_N)1rbCf z1w=}^L%KVZZV>5I0Ric5=`KM+T0x|{LqMfF76?e3F=6lhy(hkNo!|9ae|YWpWyPG& ze4Y{axW_$KrPJh_s&6%DFO&`~~ z&wjh3aT@SeX7CXoge_UNbX8aXBIQy3k!QobzyFJ=eGBUo+X#`x%Ct@HPKnh1fe}If zo+EMe-8!Atg%Ep%Z0y;vNvRA2^+`bmlUUX~Z3#8{Wu=+SV~f|<1e}VVb-nSW zp^)SFUV&L8L9J`1|IlS|hG<2S{qa3l$hYIsVGA~RUYd}@K0xU-1k7+)Q--0Q;~|I- ze@@PNt%R0GWL;gnUSJw61npGkHN#J+DjA!E48<{*Xvp;v+CsHLin(v)@R~}lqIlE} zW`9B*uN6OfK@}%gE64$|1omIdr_-E^DE@vO0xBK>x3K-Zt=@~DoKu$x4t2P9b z$Jxs6e~#)on2Q7K>ev#G7hRrX8G}2hlv7RhHz)1TIb>L7j@dkql;ZIDTj;-7&QB-5 zxk0@Q_C2iTeA>C;fa3eoq?Y%);zC)4p6o(Kk;S<{K6{eyROazPnpoG_5H>bqvtL)i zrs`56i=wqyzbIvwV=iw*O5ARGSmk22!%gnO;3Zjq4NhVEg%=Q?4TY+X{8*U1xfwFO zAXrwQv97^QF{jw+jGJlM?5z}_-B*iAOZk)La|U#cTT%>gY$P5Ga({D4Q)GX5^V$7? zN-YMv^%?qgdvXIsZ$-t44M+i-GWT!Y+NhDNZx0|+Y@zYI>~EIaGPre+yyMcRtNZS$ zd$>V-OR#$(+4008!`vaw-{cvW(XEI0L->7)7bjQjnecUBF9N%voTKKRrrE;wRe{i_ zg`%oa*el{Y_ubEV5pmQ_e-d+3a5z|XxMv8gt%K&At7l#T)QXr=_IX(=f)>J8(zl2` z__^IpaEW7R$lkX(d0EB@ksf)4Op!Xd?#{aG<8lk)7ae}IV|oxyeC3Oz|F1b}GRxqE z<#n4ey7iarzdueD+rNwpoaFdJYVl33VAp)LV#H-nppN=3kNsMW?`xVdHdvI@*5EKn zt!4l{8qV%XepI%zBMzq#zA!X6Bhl_~LFcdL(fpjvKpQ#hq;&P&-}a;oH7h18zL7NQrotWuYIcuC1D%L0QpF#EzAe zQBC=RqgBJ1yE5rqlba&C-^JGsJ%~$djIJ|@Cdh;D0cvHW0Ij*m=sw+gMB)&`v30Gw zkGi$u$)I%%2T_#HStV<&8E2VhTP*BNHg)cwV^itsqpdQ2bWZ}RU-lo+Yg-?wHA>Df zyVO=^+gP*fk3HGb)|gltFHx{S-#B`5o4%B&uI_>1aN^dLN_x`hH<+dBHJjMGGy3Xd zJH7oDeMxjm5{_1KB8m}{A>RYsW~(t|N&6m`yIV!X*X$|3Q=p;d)e)gP4+D237$Do2 zx@3wY?!2^DaM5Je>s|wmw#68`-VH;8-#7#ccC!^cV7|_{u)PkF(%gsomPVWr;0Q7diJ_5OviXZ=>vc6YNVrA4hHAC zHfE<;4`Z-Hq=qASc|sJJ9xO`%hL7Oewy}BMRvAhMXKrKi@EjkMIi7z&FrGC9+P*nU zE{X3NKF}ouM!oeJyo$@cx^V3%8Vq;M0IqQFhhJ;^?}$Uqezn)-FI8gp6o*jpyzw@t z=5&}GPAX84Q1xKX&N&D?XrsTzO0JNto7YEO^Ba5O9p)A>|4YK4?P2=H;BLZ`IzR@d zEY01xwi>y%8t2^7wR^_8OBp@#yRzTfw5RN}EQk)OR@ViLa0my)yKB1=XX=Esk1Y%{ z#m7GvS~w*_7FR(OwL9~n@nA})5Xa5&Td$o~rLoZq0oOfyIn90&F*3Wa!Lrr8sFIne zks&S3yy#*~TRBB9B7vWK$=x=U-`(fRT7w#l6S%C(oGhQ)lU)e;y5I{pjB;s}Yu(8h2UU@;@)MV#YpEU;dNSZU4HP%8UB_ zPAU8Ks6h+t9TsW_y2)cfV>RlzmzJB8X#AT&(k4O!y1)Epwm7o2w#|=pZHE}D!_Bz5 zhRlCdUKqdN^{$0D9yF_SKHP8Vp7wHK;1~FqZs-)vUB6hzz56z9m8^v?D2`HX_r9y=A5toW^3)$Y*ULYeTbo1ab|iZj zL|ZSWpq6dTn^u%|yK&=0)dnJ0P6m`*E>qzv;>_xU^K?45c?w3_ z0|6)HDi=$WFQYR*6h;7yllmRX30>ku%%jxu{MOv#SobZ(f#KBLk~J(=J8EU8GX&@- z+350Kpx#61eE%!eX{EWFBy9px<%CS2*7IL{Bx&C9V2$qeNfmW5i?7lYo8aMh|Ngj8 zi=O0(9E9?I&hQ-1EMKc-1Ys=UsQ-tSCt*dK4G{pu84nnrz-$;be4_IxCmjNX9*rK0 z;2!T*h34PUi_d4xcB1p$Hpg1-G<7)Z-xWT8l78@dkf0zg&*Nhl)6w`=HIE)(c*GnX z&iJ&s3u-_Q&7vdUN4_u8nqTaedaTc?2LyL25unVFJV-iFVj~t{;t*8VR!@1rK&Zn) zZE|BqLY=k4Bs!vgDXP+kM1qKpHUS-m4@P{f@h{_FByr(fx zOv&TSyV7wbdm0Gfl!(hI_)>sn`1^zh_lmYk`b8$zxr}qY)~fQiV^kF!Z_uh^muDdC zMWIPNL=Wsg@)G`5pq!QQ4c03G8-3}7VWv>)Z*8z)-y(1l-OzQPRpSfsNnq#ZbMZ^(fQMGbrVA%n~1f;#8zf3v#xjzX` zq8(%Q2m2CMsvb}DrT?N&Ur1DY6mj*_Zb##TAu*!}sh(9P{_w6VF#)fxie5_65533N zPo5VYY)dEZBt%dVvsM_Z_5=#7Co3PGl3PY7jZS`ibF-;qYI*!_c}!Hoblj&mtVtYP zFU`84oTh_fcpw*_9Om8oG4?^6&^N|z<0R3CDT9x?;$FiDXl7vBs$ z(J7qg&kKtH(>7i-D!Wek%PIS9+1qWaB_$g6W_4LnC7<=HQqlaQ-R<4LPRx@% zZPrcVI~HRrw4Mli;wsQ-gn?g|LF+!G9DgivWUfGbo$k4-VGPBwUy!E|8>X;}Wygi% zYJ9qq;6COlrjjS>%5)BNiDAVtgq-{v%d2kV2c^eY1#YkCtM_b{7TcNVbl8;l9!$RJ z*?7*$wJpwoFvLFG>$zut&47=LPxeT-=DWnv_1|&&d)m`|(q0Fpwx&0^S)GRkx2b?U z2N0ZHrn%>-jmzhg$A7^XY29eK)x+g0P**lv6)=-7=;YZp(eZS0>EX*w{)yL-7C-K% z`+XgMN8~bFc6t1nbV1^IZR@@l+!P-}3AuzemVc2xS-O|sR@=9WC0H;OC4G!-X>gl< zqfP4mo=f7Rf<^YU3!of$a^63O^^x4G*&(td)T1>U`0Bs4J|}h1~79(5ciPH7yRMl^;>L1zT-qdeZ8w z?Z?*Fb=ZnLoeL`t+jE3#y$vLcI4%(5_Y5-jKYgdtxy>+(IlNBaY&76T$&=+XY@hCG z__*}VKCj1g*xNGU?%t#ecx~)a`ds5`WVl7xOdpe+L(2Mww z#Fb4b%8o4xx5k7wTa(jI2k{yiS*N}k`*!%jjcA9!R0@x0U!>v;#gRu}|18f1-CV+; zjjDDUBj*bg68FA$y&-0_`N@;a%_Z+KT_>73Bl3s0W{c>iRF5r6XJanvuIA{fQZEU# z_|s#J<^%io;&OGWcsh7Qo?b2*Go*z8@f(NhI< zz$sdX#B69XW}or@;>i~xJbCM+e|Ykwc>3-4+jRlp*$a&M9M0=D_%-A4=3Tttfq!F= zP{;MNGj&>hJyg*3CcgNqs+$#j-*-w5>W4<%C#?9J_rvp_)V#q?m3_4Rnt<$3)&#JK z{K;9ahZds;%5xtTH!JM!14ro;-3?_o`G*8=FFd)i=Kp^u!LR(UBzSyjAi>MuyMUWA z7%uHrEdG1SW+qs4&H+jL*mFXuZ z01O6!XPOp);MIG1bZo_*o~h8{MaCNr?SZP{yY+=wBd_A1tq?fNY9xulS|N}%-n5JQ z9^Bz-T%i3X(ewk&O#@BA)=W{3kZ;&CQJwSqI1AzSaz;_WZ%;4^uJz?sd`oTi%Q$tN zwnn-dJ3*(9K5jnkpuRBf#NNUpz%coFN+?wCF>L%7jh#60{}YYv-Hno0nyF$gOxC?YnYgT4nir#>J zYdq8SE%B$P6m;2#rxf(qrvHV4zA3&=_}_BS2T6@&ps_tP!VdKe8YrOdCDADAD{2EB zHD#ToE9RCnJv%Y3!=sP1@*_*PA3=*`O8oQZ4LA~K0T#mGA_ASUanIGBq(C{~{2b74 zF3brq_d=u0;CHzm4cT4A=Z0I9N3yai>RC9mDdL&Cvav77bsMA!anx=iyuoggb%lQt zHTd8E&;R*~0*Iy!lBY9)5r*dbD=~48owc#v-yOf6!(cp{79AdK@#Jqw35<1U!FF_T zKzqi5ycmqQ83fU_WaRfTUah_=(LLDR;7WT8H0%cyFcR?d%M;xy4Cw?Ago5#$==goJ z$=dIIeIK*4F9l&D_l?1+c@?GvL`M&&<|u#&69It}(9AiBz-3@ytlwaBSbqjIY3W== z;5fp>$@eF`?Yq}p9${DVgOCOv-(SE6hNJCt6DZ6!UZCa#^A3Rld>SL4@PxsQpK1&N zyfR`9tX7~s|KvWoW`cA5fw{_sP7V%0M8+;ZgIp1eKZjLpe$dWR~oj6fd#0fY3AJ1$K%1R)^gCo!h7JwUJtzHUBY z%ENJ;H%-nMqjMgg}m584%??TD_t!oL+AWQZ_%8rEoZtW#r(9JOq%R z;KR-+Dq?_%q#hZk6ku1Nse>Hd@BOxMP(nyd)CR)$`zI%l$!KQkiw6iaEn206wmgvR z;TFD}aPIrh!!u_$1U@au#+XG1=n`yx9+JGPN2 zK!~DYE$b#%;BrrX;pCQJrIDLOmKpg6c{a$kMw|zNcsCocfuWlKh}f{NfPxMhiUWpF zx_RQflb4%o3S&r*xK4IY)-{mO`PCHE(hx=UxXpb-`>Mg~$Ct_AJ$d%-TVUpy{Q zT~G9*L7G1HIvFd4Y*MY%ku`$1Dbi_Giyl+I@$m5a(FP(Ip2B9y+(Fli`x7mZtg^C5`4SMVZBpN&2$kgJJ)B{Z5=Is#EDUAe z__!Hxvv=t4+_?i{epc%2KcKMyS~k+1XO@@aqoTYCkvC%`)svBvL%&F5WF(9rQH)lS z?SK)D(&FNbORqqL;+GwG3wVr=fWTZ*UTzq`ZXjcZOq|VDSe%-g0^Ga1?!U}#xqXW)29_(fJ=K48rIf~u$7SjL@$ zi(9{Ju|aJwPQjDebg-MpS20&q`h%UVEt{qC9k(j zh?0k0+WMslJbP9VBBuE+o?}7h?B33tMQIo>i{bF?-1+lBLLVC)MMOc}LeK8~`SYs` zy&Abam*@^6b?`ugMad5Lx3_k7R$~&lx|6hy&%q{pW!MgDyt2IPDebbdvckZlwPvAO zceKBBbol4b{{H(XFMj@6t8-t%zM6$gK)S%_iUu=P9OQhPj*c#R zJs-b3&;4*a%G=x9%c~J=@C_Do&^V2crmZ{JZ1cWKl;QU?k{MbxK1Hub%!FXCEPD2HdaYN;e-DE?++H=+0{TImfH!nO@a)g0>bFJ4L`Q@7rE>2U3|R6eb`x!bm7}4BPPOIz?IoQe zhlSqsymoQ=8og)F)`zs+zgIi<_4ZnWc}I@?`}cQCqrz|ByjcoOVAR67u77s8Y!|~} zw(h%|G)a68Z@}2`N>jkhb0*6B_s^E6uo}x47-Yahj*XE0f| z-$0&F0!7Po5!pwtOyt$nsFwy{v~nmjf4W?jbMPOb73B${H4`I__ zFeCDb9QLbeJo`~}nqsu1`FBBJ5C+t|gCi*Gx4Tk4DQ7Bno|KeSrAa_VCHkOe7=IKb zNX=|VON?1qSYXXtwA~MFK}Hqay~&5{zqnm?tw7S!&W`1+=r3ePzrx%mnUB)+b4s-! zK#y70LAOjGULY}#6i;@7daX;v+qamdUhM4bpnD4-0`^Eie&u}oKxDTdWASV!L+FWRL zdYGQv+Y8e+Ct)}_dYOVz1-saV+MwWp~A_YR1vQ;Abtp&1RUF6fBgk&n^E-I78R_0v9Ktx zahFFvw!}*%a(6r)SwCY<4w53H3NkXhtkW@FMSu*#nTL)JaPmSoX4O;U!6Qb)w0gl5 z80D^3W3zypjU9J&y0GpD*RRp$wOK|fh!{e&2U|206O`|Q^=r2}Ibe~(Mah7F*X0t( zj3D_xicTKvckfLXQ-65=%#R)gPtU@vX|N0=_Rsg=t}+7yJ1cFMrVZ6N+>bbEj}O<} z@gE!GL#K}ycE|gbJElmcT7SBDmDn?TgXe}xk|E*?qv%NBh|;g!_(llZeA4~6=0L&} zac~|b&60Jq&goS~Eo4kxnH+0H?x*~sFelsX*Gpes9 z8VA_9uU5=WGd46dK$q7fSb=ZfzJWz}|EMXJ-Kg8^GX7g^Vme%3mAtbcR4F3TWKpZD zaADIx>8~OcPP^sdI=JQfLD=n!3kya0u)#Nfb=?9pb-!y>XMXd-UCEhK`vCmys*prW za|qet+i`urHaGi(&%jd+KkgABhWOBU{60JTK5S6h-(4lbq2VDR_Y(K^<`Tg#^%kQI0+0Lq)w0*Gzku&uurxJkhaD~wl)+(%&!um2MzxEyG=aVbjmvC} zwp;+)nv{RM+r;zm;lnI3y>Dn~I?$b(ZVAH@5fvA|zp~m|QEAOI&`ZMkbuYJfh1U-2Tj1!2|-k`*zv3fh0Zo5ws2TTLwr=3e|>&^ zRv{Pcde0^}#Szm%sX$O`ssuAupioL9B2NpB;j1_U5}6X9DQQgw1kRRf!9FLt2@0#~ z)ea2ma6%HHHP&mE0sKM5g7`n?1cPb*n))C%_wFmTa0p9m6ke$n!$8;%Gc?KtGCzuoAD4+q z78Vq&!p&x%=K{TLkWeWr+kw2pYtSAV21{`8@kMAU74LI!#1XWC0K?^mX* z?HUmq2AhrB`u_e#6Y=F#0VA4H!;%B=48|rVaxKJeyRQj4)5W8(MC5mIWwHby_~dtc z{yh0n)oiKh5Jb7girU)8r9)csl(+BRr9-1Qj}NvKmccFV!8q=1G#c~Dtw+$oyhAPo zcy2Ov0Dhz$B#4MZ zxC*RK#di+^TIj&`lE1@d?b+H*B;O1MKl6aTc9;KFT(RpPJJ4w`xt#s|<~ztBp!fB6 z0PDoFTu4LmXx(9ud{#i_dFsjTZr=Pn7jq8w*j8hl_s^8yBd!iHPNfa78bXp%Ha6(% z#)2@BFIzIjz<&n@mf_7*|DJFzxlC($h)-X4P+U)?@-ZqnI4PJI7nd|dRHc~>*%BHM z=etq1Lx?8LwyC=X^ZYnr;|TfTNxtXYLr#sy+Q`QykmhC^FWl@=kb*r@wfVQ#VTYJ= z_4h3{Ydt~1uV5mQ__(xz5xOM}b`Pus3%grG%N^CMNZ`)q6@&CSga zwt3B_Uq*w9JarEPu>(^?9~{_%lQ<6!tQ0)q>};yTMGzuSsIBE*QuD^#wnE{))rgLc zu15EYgYn*mT`^DZ29Kc37DV6st7~>fO(6mS5rYKQl!sxhGH1!#R|v0P7wGr_Q55$f zx({Z*_o;?2S^fNa$)q{^3-`;}c5oqLip6DR0F@lVsZ(E- z*!8t~6Ico7ENiy$;#7hH65+h2>zjy;40Z-(SQB_s3`bA^@DFC7Kbhx`C%wUf@}oan z77$lnxO&IoaC^w>$*oh@%5bQ~VY_$(CfqyO4&*4JbHi44C!E(pc)@ClxHV@=kn4IS z^GRpxbJ;CNdw85Rb;#ngwX?(fmJ8wWU{6oTX0{@S!#WAAOhW_9c}&c}yIg8NWS~bi z^D)Q<^GiX71ya$aA>oBE;BpqF{Pu1UbkqN)pHJ+D{?w5S5HG-z#{{b(|J`Dux`pL% zy#ZU{xykz(Y$5gR8bfWiYjA4FS18;0Ct7+G6kvl8@BY0(IMJ*R4pG11h{I zs$a(*bcc-;&o4JO*LVbmYYq=Pv_~^cv(x_OPGV+emV}t7?#V*=B8-lvVx*+>LC!=U zajdYgFopE{G^8uG-jY{9pn+DstozH-(o$0paUm`vO^%L4QKI`%MMj43QO(cJDOAtEpt7Q(W^XnQG&jMBJ#VYIWQa7%T)p#z#ARax3CYQCpoXvC z*H*jPN^|($<}?qVgsB7MMSjg+zugjgB`VeaavAKp)W&H*9*p`UqPYV z4>p=2ozL=&s5CeEC!{Hx;_(H^&dC&oTMjVl?pIy?YwuuMQ~hKvvtMq=jidLDs)QHt20qjE$Rqza&mrze8UU>tjKtnnVCTz zrJz?J8E#fT@Ht0<|1jSkbIEyVv(Fy#R2oH6}j(VF4f5 z!-E``!~IflWqnB?hHx~ssc_~gXqST$!F>Ys!QRV`9i0JDd;LNCazN=in4{ieGN5#7 z0=@zK3)+{10>Q3CzY-%r1Hi+>W0%}W#%~jza!cPi>~)vFe-E0Fz@VztL=~j z0ni`*5(4v|CqP9TTEv5<#s~c?|VPpjsQ=;AnosT ze8**fArn+JemZ)-!5N~8O?CXp)h4T;F#Y4lm+kKmD$La1W6Gj+yYUN9*N2Kma&ov) zk6}cMv$OLCtUF$MNlExBCu=ju@7JM697C-Bi>oUu=FgvByM8?^Iy&ne=FHsO*2fLz z`GdW!5@U$CpRPRtw}!07t0c@Cx5H7u>L7bYN2P!GVD+Qk3)Jiv4`1RC6BEO<0Zvqk zAUg~Wn>cp8xm0CpV$!-+C2l|E+B#ltNlaL-M$4o|z$7Z_0jejTHolCQN?DIXHtJ^y zdy*3EJ~%QGbJVxtY-CMfbFlj&2YU!XXLgpMj(Vg3_KjQGN5i1G$6t z$`RsIg_!5gbrbIkzszxH5)|{m^fkp{fCw9uI*2&zzy&2RSAd^aNrmzwZ7bwL%~F7t zz}!=qpbQW^EKw6v7K{g=CIuXpgTon|YssD4w_A2%84a&;6?D^q3wreE(G+V4c|!UC zp9f}0^XSM(&n6%rlT@Cbk;-SXLUG~W6+i?_!VP`K=`h5xj& zjM^)wNi$ak=jAz^;<9aC}SNR{ymTef^jvCGig@hGau?Y`SA-Jnr?`DU{dK1 ze|UZt^NNf_CZ%|Jtm{S^M}>YHo+3*C0rNX7PcRwybQ)GxR`ckq_wU~aF{3hHw>dL{ zc*m{Tkh?D`1G1dO>3(x=L8V1FbMWJFlXutt}rrXtY z`}S=Y6ojhq~-wgl_}q;4*%t?k?qE^5-jC9i}gyxbE9HSwl^t7-ln+mm_nF zn(`0~W-XU*`O-rGrTY8BQ@DNK$T5Qg2;@fh3~?7jTYK?o)yPu#__=h2n0$^tC%9iQ z7OSyJR8ib-rCzCOOLj^$af(cjhjI&EFG)q4tkVyE`zwGM@$37(gftO`e1Q5B1o8WQ z3i9%O&vU^ezk(UZh&U9Cmn!(~ukkRE!=A2j8KOefEOBaa?IInoU`$`Mzj?0+!^U-- zHJY~lRkxiHL$S42i>^6wX?*Zllt&%y&m184K zWz^J04Z1aY`})-7J!Di@3So2^Ms*aXtY@H~AHe%gU{v_|&j`)iE)BjY(tsNy594y# zU2mhWBfF%ulh*8jj3T)hJSvu9JkC*9pa`325U?R6ixBa`dK$=7Al3n5@Ke98?t zl(70j@6lG`G1-F$C|DJ9#Ukm_c+4Knl!5s8JA>`=tJZ>a8Vf{WG$>1t^-+unFB5?E zx=9JpfPh*Hh>i^pLuJ>hfl2;hd3iYnPm<&`3J&@&mP@uz4`-sZGo%uzBg9m_has09 zJhoKUOMYL(|2=|AHXHW-t9P#Y^ctVK3$=Od25gLB*wJ|KU(`F<1_Xk3%}OxV?822> zyj06!DL-c}YJTd4^??cF8@PX8gB)=qlnx3ZzQe=Ezl<_g(bPO#9${=G@~TYZc0bZc z6DS5@z6?^EGt@f~t}qnZ&Fu*Cvzx5Uh0?Y#AEsG@m=wPN9q1bllZ~qd^^=i>&JywP z?4J5m$qVBmem)oEDpNufRgF61K$zCjTnPvVU`5Q9lQ4h_j28_}E@-yB zD~7x;^OmuNg%->}PzO5;vO*l7Nd&UbFz^PPF^mGL>B`PgdB!nFa+4<4Yw`4&08>rk zlk~}RXHH_$+!m~`Nf$879iFsm4X48O^&};Y3XO;u0FV}thm@o5w|>Hab6I8PY?Y)) zBbi)9rQ89(QJNbkrBhxHg?D*?;p#R8uJoo~=K5KvR{HLEoX5DA%xQ3Kw3LRB>?!32 zWQS1A4c9R+5boMQ>q+|pJjBGn&@(gy>PG-TM0VH4v6vG)u}T`J;&R7jSF=EM-UrrKO~}U;OkG)d77;FGXq|c*ON& zacs7E>+2>MAURehkiAaf^$ zw^_*V?qfj!p%@tElCanzIh2Gt&2JuP}Y{+hsx<9 zp%XQXwhBa%ya&`%YYNi$XeLNWh{4YSR&3bUP&A^ZQiZ38)&IVXk5*H4F(2RdyZfoX zV)D;$D&hA~qbsg;*m!ehtlDvFej)J<1_eNZq;Vx9Y# zE(m+kg*zM_IF`ku1EC~>C8U&o9!s@tyyE3dFDPSyE*hX7jndVwf64$|>yEd$6BrmM zpCRGv?~mTz&d<+J-|Xw_GdDA1F&id{l27C9Fp;5Aj&xcQqWw*LT4KQCi>NJ*UFpF> znItzOb{0z%eKyN7Iyf*=RrgvTl$qM&*}~r3apz}s(*cGASCu@8!YHY?^9YGn5-DhF zK{4Wh^Y_|hW=)nv`a1z0sf~9&fq_u|UK_|ALMo60jg4L%zT%BJT1f`XS=%(?0oy5& zfA81eZ~Lt#Wg;|8Sze^3z-&=8b#QR7v)hSg&^3yAKQt1gH&N{f(8fbr+H@N@|4`|o zkxXj`t+E>zp+==9CkK&O1mxTw>-8Vsz8M`BwgpwCODp0{iaAs8hTeez+m|n^oVFLG zc@716csMvX?%z*!{=Ld@50vo!%d-&aLy~9Kl$1mQsd2%|qYFenHO+doRxQ7bW8N?r z-r_%R5`!_#z6%FoYyQfiPzVs<5&+hf^NdX>wHg zP`EJA*DtcljTV)ex&m<&E+v$-EYb5lu^)Z|IfkKj!v5cwGf9#=M z${lT{Uga{a)RP@epgMLPzzZ|MUo1rQ3=J^%0x`uBJe5bF=|Bz)~d2VJ#QAz2Wx%tl2FSREo72lXIMx6iqx3*(fX>z|H7oH!G zhju10E-Nl>ZEsI|Rq1~0hUEh(3rzmTeDG>vR zGUe_IDnL>}G*Jger*rLovI|rzC?)7Wq82y$bP!32`rEf0iL0m#x5qGsg@!^VZfIu4 zX+By4;8}#Z9DCU=c=L3*?4Qr#fSv>pe*ykom+`hXQ_w#&+XJ2m#1nI!3A(vZ(uGqv z*eTmqD&{e2qmKZ}Hb-!&Kmy5C%D1++pBv@8go+}@!NIwCvoSJ}(~&>(i*J;uv6y!! z=&S-KrJLkINJM0Szds1~0RSW%c;{(RfS-U^6JDL&3c-wOom-8@J|vq|HDCg?o~^&Y zG6w5oHZnPx`1qk(iBF_<>jm^RSp|E zWlxPhx%4u_$tgRS@3}wtq`0Ry{AvPBz&Y{Qt@m?FOKtV_AE+bD2FOi>3g~EQ1Nb`u z7Mbp?+h3#+tAqM}&F86KAd2|>+G-Hb(IXZX&`7L`izEC~jbQomH)-#bYK$>u1t{TB z;ook7sjHtdgaAa0|76vSilT>bHQo2hzdtEPN>jJ+R8a67m!7=m^~y?cy0T0urs4l( z{An4dF^M4u$-DyPA;ZpihR2Un&X1yZH+0!akI$8u_gkCykoEB|?OytyNPjb1ia^MK@+lEgReH4IyYf`I$-CWKV-DyWMWW!l?`T{zjZqBzX%W zN{sFz8HQr0b++ggL#*nyHv=-Ul>llP#)SvcN@-Nu_!i6hRGyk;o1JCVf9>*s-&Jm@ z^Tj=wO%+BFviSSu?d7aKh7Rzr< z9w9cAr`o_mMMRg_Tc};PIyJ?cOU6zi6`yoou0UA)IsrTQScF~qB?^V^rtckA%K9S> zPex}f9I+P2sTklM4n{@z8e(^H7SB)Z3n->n3|5H2w> z54Z+^Iebdw7-wf zGY?ji941IR0e0gh>?7%(MRGXg+P?m6YX9f|&i@QW<2uZ>3*QA!(>?eJ_z{Fec*-gK zn+$nMB82}FHZk%-egR$(#3L`{7vS~Zf9!;w_}_mOuk$A;4MQv-AtAy3*ET%cN1#9n zLZ3CRfAH!~%ETD?zX0U~eR5lC>rqJUfCJ!uZZS14&kV3zVpnuXRh9Ge=l9&q5Qq~) z?JM{S5&v6!1PZFEmrRY>K=;?k(9j-2tb^&38!PdGimpT=H&JXa76KDARhJN}cejh)Lati`%|^=(24IUsxxLGP|078JuMTAF*0jE3g2 zNpI}(WSu({fB*T*Er9JH9A5V*ksw!xsiM8Rw8hy%j*z_99c@F7RBYF0-`?GgQ^$3} z)08~UT!Rs$z`k4>D)gHshB?VI#w98@KbGFH zVf*{%csFA5)@nE(TePRd^v}dwg9{h2?@~A(E%WrkXE(P40({y&jc6z4*-v-xW@mC`Kn=^cB^8Pn4?CE^~GV)fgb@!0Pa*k z2(`JfaS4UeZ?j*Uu(h%p{%DA;o}ZU@j{QG7+~0 zhrvbz1-4+ZC$;I#`OAR&Ql4e=Ou>X&UH}nCB<&;wRmJ(*b?@^#qZvG0kZHUO3=C@iS80>)L3 zR{YQPk3Ge{zL#zjMl3HaiI2>8|Gk7)bNC=FIsV~IJe-r8OEYJE!`K=f>GN_-d7_jrm#U0kCUcUo1f?)f5s&+Z$egSd~P`Y5E zkA|3=8#h~UP*9Le7c-rgX9iGypgedW{rB#~==Q-}SX>05?1RbUBYQKmhsI4QLVh;_ z?moDdwzyycEe3Xfu#@-&ru~?3VM5EfM=sK>gV}i-zP=I+`Ds6Jt*Vor39U>96$NA6;g;-%;Lhx5O*RNm4!2xA;egT0h z;7;c%(+_?ifV)Aq&u9ha12VGRKYzgY0)7WGX+K`WM|?MYI_c0fm<6V0Aa{<4Af4KG zBqBfg=t+I!#Kja#8Co7)$%x|{oA*GGGLcB4lD)jo)>6n25b8E1$~ zDn!Lq85AT9GWn`+i*%dhj-Yf#Oqfvx8r_WUbWW#xgIA>mFf{fi zug2@)$det43}n2dr==}bPD)DR1qD642R7*XdVVp4-y0K_oP3Wq_z<=%#giv07P0?v z6QD;fgLQm4#0Qgz%PIUZ%o{dy6XEB7BU<^60AS>>ER2efcLZeCXeik4@9lZLeH(b2 zadx&N&a8L<>MMC_rHC3la-BWQJ}xPzht@3OAzy(xj^Am5j?vwKmQ z;J}RNtkhJ2+d$2SydUvr8)&6&l8{t5mY?L$0zz3raQCiM8=1iTBkYrXi`u7$2Ab&$ zdbh}~gB1Xu^Wwz|vpz_hAY6JF<@nA2_VgWvz=8t0m@o(Sx^uI1@FY4IFNLId27+HN zC@e%`T_tQHcEQ`=GLgUwwm>(DBdGGLo+a2w()#;!=0s1h8|}=)9l(2Dm1XjQivb$j z9)sY!T+NhLxa z_Tr)W#Avwwb3cj#GPrnHW%zQMk}RNM@S7=9`; z8d$;bI;f2ULlg?LYlVDJ0|%XTYmulHJ)i`dO)tTWQ!yskb|obm)ec1k1s16U;Smu+ zzda>o>VPN?^>e$`@evTzx5H--9&hZWgE8S6vtbm{a!y)WcmFoBxy;Q?O_{KaW{%6l zMJ3Fnq;Dkc#aKRPXgojxXZKzlJk^cu?Q~8+@* z2mt#OP45M0lV!{eGGzGOr}cc9FQ9OBWJ(Q`DVWwFcfI)bPNh%3A;|uNoww0B*z1Ak z&>2^sG&`^X|;0yLl#)+iD5Ayc3cALivVHiY22_+>=YG9x<4%#AVHNN`LC^092LBJ0|)1`5+J(35rpd_Gx+l6JfL%#I!ytD z6j48b#}Y0TnTqDM#*d4N5{VfZL$J8pw{CrHYJ%1h#4VL*dSG3?Oa-_)4!$TJC|NEGI=Euqlp>4~^XdcoLO1F<}jZWzKFShqB-<}xD;`9F)%kdXb+rYg^ z!iwuidxMa$ZEfIzAw{^9dd0$wq8Vui%x&g?lpXjN5RBME`BAbr3E~BrBWSY--lD2i zM;t1WIb>tPZGgvwDG#A}-3Wi`=`~7q*o0wBu;;v;XXNGN0NM}^x|=`IxdmZeYN|W1 zZ^Ty8^7164rGs1oonNP=rlwY6re|cx{eX^%soKLAD={~iecX$d)o)SZS6Lnmil3S& zjQqi*25;<-k*qiV7&CKn#z*coY-MHfLB^gb33ZTrM9BkCQ!EaipP%=`r=OjkUf1qr zx>OwI?|+3FG2UPq;8_73iQjG$h|0DLz52k#Ll|EWW-1tycD#Tjzx8;;FtG=98=4Ae zUkax?z%G5Dk_#*}({$ILCy^!$j>+PinVA7(1`e#*HlRJ>6y)S@+a5u&5BjsVm?>FU zlwQa~M(0e#-}`x@1BtRw82P!mOE7tx6BWyDYm#XX^97)7frg9W8kDyv-rNkU>zHR-Gi+z)w+u9r6C$FIOEp8&3jUWZn2&mm81@%U?(bSYSO}= z(ifCuWImVjS=GVYZeG4GUFV0?hqm%@k%8rf|I7k2f39bo(9e5V9qhspuuhR`Ksc`$MM zReta@t1ycY@;d0UNO-lM^k)w;8(?1lgji`e=48`Cu8$mHcp=F)ybuowFT^*&3vp-g qf><> #$originColor { asset: wS } -object "OSonicHarvester" as harv <><> #$newColor { +object "OSonicHarvester" as harv <><> #$originColor { rewards: CRV } @@ -59,7 +59,7 @@ object "Special Fee Contract" as sfc <><> { asset: S } -object "Curve AMO Strategy" as curveAmoStrat <><> #$newColor { +object "Curve AMO Strategy" as curveAmoStrat <><> #$originColor { asset: wS reward: CRV } From e9e4887ab7f1c471ae225bd4efc9fcc906e7fa6a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 24 Feb 2025 17:10:37 +1100 Subject: [PATCH 25/80] Added withinRange Chai matcher Added messages to emittedEvent Chai matcher --- contracts/test/helpers.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index f0ebd61011..9f29772a3d 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -7,6 +7,17 @@ const { BigNumber } = require("ethers"); const addresses = require("../utils/addresses"); const { decimalsFor, units } = require("../utils/units"); +/** + * Checks if the actual value is inclusively within a min and max range of values. + */ +chai.Assertion.addMethod("withinRange", function (min, max, message) { + const actual = this._obj; + min = BigNumber.from(min); + + chai.expect(actual, message).gte(min); + chai.expect(actual, message).lte(max); +}); + /** * Checks if the actual value is approximately equal to the expected value * within 0.99999 to 1.00001 tolerance. @@ -136,17 +147,22 @@ chai.Assertion.addMethod("emittedEvent", async function (eventName, args) { const tx = this._obj; const { events } = await tx.wait(); const log = events.find((e) => e.event == eventName); - chai.expect(log).to.not.be.undefined; + chai.expect( + log, + `Failed to find event "${eventName}" on the tx` + ).to.not.be.undefined; if (Array.isArray(args)) { chai .expect(log.args.length) - .to.equal(args.length, "Invalid event arg count"); + .to.equal(args.length, `Invalid arg count of event ${eventName}`); for (let i = 0; i < args.length; i++) { if (typeof args[i] == "function") { args[i](log.args[i]); } else { - chai.expect(log.args[i]).to.equal(args[i]); + chai + .expect(log.args[i], `Failed to match arg ${i} of event ${eventName}`) + .to.equal(args[i]); } } } From c656828d92b8ddc74f7c395d1db0ff40510aed90 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 24 Feb 2025 17:14:01 +1100 Subject: [PATCH 26/80] Use Waffle's emit matcher for events --- contracts/test/governor.js | 12 ++++++------ contracts/test/vault/compound.js | 4 +--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/contracts/test/governor.js b/contracts/test/governor.js index 3208cb8a4d..db11f13f58 100644 --- a/contracts/test/governor.js +++ b/contracts/test/governor.js @@ -232,10 +232,10 @@ describe("Can claim governance with Governor contract and govern", () => { ); const tx = await governorContract.connect(governor).cancel(proposalId); - const events = (await tx.wait()).events || []; - const cancelEvent = events.find((e) => e.event === "ProposalCancelled"); - expect(cancelEvent).to.not.be.undefined; + await expect(tx) + .to.emit(governorContract, "ProposalCancelled") + .withArgs(proposalId); // Expired = 2 in ProposalState enum expect(await governorContract.connect(governor).state(proposalId)).to.equal( @@ -270,10 +270,10 @@ describe("Can claim governance with Governor contract and govern", () => { ); const tx = await governorContract.connect(governor).cancel(proposalId); - const events = (await tx.wait()).events || []; - const cancelEvent = events.find((e) => e.event === "ProposalCancelled"); - expect(cancelEvent).to.not.be.undefined; + await expect(tx) + .to.emit(governorContract, "ProposalCancelled") + .withArgs(proposalId); // Expired = 2 in ProposalState enum expect(await governorContract.connect(governor).state(proposalId)).to.equal( diff --git a/contracts/test/vault/compound.js b/contracts/test/vault/compound.js index db8490cb16..aa668986e6 100644 --- a/contracts/test/vault/compound.js +++ b/contracts/test/vault/compound.js @@ -35,10 +35,8 @@ describe("Vault with Compound strategy", function () { const { governor, compoundStrategy } = fixture; const tx = await compoundStrategy.connect(governor).removePToken(0); - const receipt = await tx.wait(); - const event = receipt.events.find((e) => e.event === "PTokenRemoved"); - expect(event).to.not.be.undefined; + await expect(tx).to.emit(compoundStrategy, "PTokenRemoved"); }); it("Governor can call setPTokenAddress", async () => { From 557ee3b1ce8fac5e1cbb20717d662dbecf2f6254 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 24 Feb 2025 17:35:59 +1100 Subject: [PATCH 27/80] Change emittedEvent matcher to use exist instead of undefined assertion --- contracts/test/helpers.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index 9f29772a3d..59125f272b 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -147,10 +147,7 @@ chai.Assertion.addMethod("emittedEvent", async function (eventName, args) { const tx = this._obj; const { events } = await tx.wait(); const log = events.find((e) => e.event == eventName); - chai.expect( - log, - `Failed to find event "${eventName}" on the tx` - ).to.not.be.undefined; + chai.expect(log, `Failed to find event "${eventName}" on the tx`).to.exist; if (Array.isArray(args)) { chai From 5fbc481231d5b284c93af43c2c40e72a85b5b786 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 24 Feb 2025 17:40:55 +1100 Subject: [PATCH 28/80] Use new emittedEvent Chai matcher --- .../ousd-maker-dsr.mainnet.fork-test.js | 21 ++++++++-------- ...o-guantlet-prime-usdc.mainnet.fork-test.js | 24 ++++++++----------- ...o-guantlet-prime-usdt.mainnet.fork-test.js | 24 ++++++++----------- ...orpho-steakhouse-usdc.mainnet.fork-test.js | 24 ++++++++----------- 4 files changed, 40 insertions(+), 53 deletions(-) diff --git a/contracts/test/strategies/ousd-maker-dsr.mainnet.fork-test.js b/contracts/test/strategies/ousd-maker-dsr.mainnet.fork-test.js index b06a9be013..b44b1b935e 100644 --- a/contracts/test/strategies/ousd-maker-dsr.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-maker-dsr.mainnet.fork-test.js @@ -220,17 +220,16 @@ describe("ForkTest: Maker DSR Strategy", function () { log("After withdraw all from strategy"); // Check emitted event - await expect(tx) - .to.emit(makerDsrStrategy, "Withdrawal") - .withNamedArgs({ _asset: dai.address, _pToken: sDAI.address }); - - const receipt = await tx.wait(); - const event = receipt.events?.find((e) => e.event === "Withdrawal"); - log(`Actual withdrawal amount: ${formatUnits(event.args[2])}`); - expect(event.args[2]).to.approxEqualTolerance( - daiWithdrawAmountExpected, - 0.01 - ); + await expect(tx).to.emittedEvent("Withdrawal", [ + dai.address, + sDAI.address, + (amount) => + expect(amount).approxEqualTolerance( + daiWithdrawAmountExpected, + 0.01, + "Withdrawal amount" + ), + ]); // Check the OUSD total supply stays the same expect(await ousd.totalSupply()).to.approxEqualTolerance( diff --git a/contracts/test/strategies/ousd-morpho-guantlet-prime-usdc.mainnet.fork-test.js b/contracts/test/strategies/ousd-morpho-guantlet-prime-usdc.mainnet.fork-test.js index fa3f43e112..c85fb37567 100644 --- a/contracts/test/strategies/ousd-morpho-guantlet-prime-usdc.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-morpho-guantlet-prime-usdc.mainnet.fork-test.js @@ -250,20 +250,16 @@ describe("ForkTest: Morpho Gauntlet Prime USDC Strategy", function () { log("After withdraw all from strategy"); // Check emitted event - await expect(tx) - .to.emit(morphoGauntletPrimeUSDCStrategy, "Withdrawal") - .withNamedArgs({ - _asset: usdc.address, - _pToken: morphoGauntletPrimeUSDCVault.address, - }); - - const receipt = await tx.wait(); - const event = receipt.events?.find((e) => e.event === "Withdrawal"); - log(`Actual withdrawal amount: ${formatUnits(event.args[2])}`); - expect(event.args[2]).to.approxEqualTolerance( - usdcWithdrawAmountExpected, - 0.01 - ); + await expect(tx).to.emittedEvent("Withdrawal", [ + usdc.address, + morphoGauntletPrimeUSDCVault.address, + (amount) => + expect(amount).approxEqualTolerance( + usdcWithdrawAmountExpected, + 0.01, + "Withdrawal amount" + ), + ]); // Check the OUSD total supply stays the same expect(await ousd.totalSupply()).to.approxEqualTolerance( diff --git a/contracts/test/strategies/ousd-morpho-guantlet-prime-usdt.mainnet.fork-test.js b/contracts/test/strategies/ousd-morpho-guantlet-prime-usdt.mainnet.fork-test.js index 3fa272ce04..c2d401f1c5 100644 --- a/contracts/test/strategies/ousd-morpho-guantlet-prime-usdt.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-morpho-guantlet-prime-usdt.mainnet.fork-test.js @@ -245,20 +245,16 @@ describe("ForkTest: Morpho Gauntlet Prime USDT Strategy", function () { log("After withdraw all from strategy"); // Check emitted event - await expect(tx) - .to.emit(morphoGauntletPrimeUSDTStrategy, "Withdrawal") - .withNamedArgs({ - _asset: usdt.address, - _pToken: morphoGauntletPrimeUSDTVault.address, - }); - - const receipt = await tx.wait(); - const event = receipt.events?.find((e) => e.event === "Withdrawal"); - log(`Actual withdrawal amount: ${formatUnits(event.args[2])}`); - expect(event.args[2]).to.approxEqualTolerance( - usdtWithdrawAmountExpected, - 0.01 - ); + await expect(tx).to.emittedEvent("Withdrawal", [ + usdt.address, + morphoGauntletPrimeUSDTVault.address, + (amount) => + expect(amount).approxEqualTolerance( + usdtWithdrawAmountExpected, + 0.01, + "Withdrawal amount" + ), + ]); // Check the OUSD total supply stays the same expect(await ousd.totalSupply()).to.approxEqualTolerance( diff --git a/contracts/test/strategies/ousd-morpho-steakhouse-usdc.mainnet.fork-test.js b/contracts/test/strategies/ousd-morpho-steakhouse-usdc.mainnet.fork-test.js index afc1e01f5d..9dcb7700fa 100644 --- a/contracts/test/strategies/ousd-morpho-steakhouse-usdc.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-morpho-steakhouse-usdc.mainnet.fork-test.js @@ -246,20 +246,16 @@ describe("ForkTest: Morpho Steakhouse USDC Strategy", function () { log("After withdraw all from strategy"); // Check emitted event - await expect(tx) - .to.emit(morphoSteakhouseUSDCStrategy, "Withdrawal") - .withNamedArgs({ - _asset: usdc.address, - _pToken: morphoSteakHouseUSDCVault.address, - }); - - const receipt = await tx.wait(); - const event = receipt.events?.find((e) => e.event === "Withdrawal"); - log(`Actual withdrawal amount: ${formatUnits(event.args[2])}`); - expect(event.args[2]).to.approxEqualTolerance( - usdcWithdrawAmountExpected, - 0.01 - ); + await expect(tx).to.emittedEvent("Withdrawal", [ + usdc.address, + morphoSteakHouseUSDCVault.address, + (amount) => + expect(amount).approxEqualTolerance( + usdcWithdrawAmountExpected, + 0.01, + "Withdrawal amount" + ), + ]); // Check the OUSD total supply stays the same expect(await ousd.totalSupply()).to.approxEqualTolerance( From 2bcabd1c8b9c4c42d399fe9245cd8821128db135 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 19 Feb 2025 23:36:41 +1100 Subject: [PATCH 29/80] WIP SwapX AMO strategy --- .../interfaces/sonic/ISwapXGauge.sol | 52 +++ .../contracts/interfaces/sonic/ISwapXPair.sol | 95 ++++ .../sonic/SonicSwapXAMOStrategy.sol | 409 ++++++++++++++++++ 3 files changed, 556 insertions(+) create mode 100644 contracts/contracts/interfaces/sonic/ISwapXGauge.sol create mode 100644 contracts/contracts/interfaces/sonic/ISwapXPair.sol create mode 100644 contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol diff --git a/contracts/contracts/interfaces/sonic/ISwapXGauge.sol b/contracts/contracts/interfaces/sonic/ISwapXGauge.sol new file mode 100644 index 0000000000..d363ae116b --- /dev/null +++ b/contracts/contracts/interfaces/sonic/ISwapXGauge.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IGauge { + function balanceOf(address account) external view returns (uint256); + + function claimFees() external returns (uint256 claimed0, uint256 claimed1); + + function deposit(uint256 amount) external; + + function depositAll() external; + + function earned(address account) external view returns (uint256); + + function getReward() external; + + function getReward(address _user) external; + + function isForPair() external view returns (bool); + + function lastTimeRewardApplicable() external view returns (uint256); + + function lastUpdateTime() external view returns (uint256); + + function notifyRewardAmount(address token, uint256 reward) external; + + function periodFinish() external view returns (uint256); + + function rewardForDuration() external view returns (uint256); + + function rewardPerToken() external view returns (uint256); + + function rewardPerTokenStored() external view returns (uint256); + + function rewardRate() external view returns (uint256); + + function rewardToken() external view returns (address); + + function rewards(address) external view returns (uint256); + + function totalSupply() external view returns (uint256); + + function userRewardPerTokenPaid(address) external view returns (uint256); + + function withdraw(uint256 amount) external; + + function withdrawAll() external; + + function withdrawAllAndHarvest() external; + + function withdrawExcess(address token, uint256 amount) external; +} diff --git a/contracts/contracts/interfaces/sonic/ISwapXPair.sol b/contracts/contracts/interfaces/sonic/ISwapXPair.sol new file mode 100644 index 0000000000..ce245bb121 --- /dev/null +++ b/contracts/contracts/interfaces/sonic/ISwapXPair.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IPair { + function metadata() + external + view + returns ( + uint256 dec0, + uint256 dec1, + uint256 r0, + uint256 r1, + bool st, + address t0, + address t1 + ); + + function claimFees() external returns (uint256, uint256); + + function tokens() external view returns (address, address); + + function token0() external view returns (address); + + function token1() external view returns (address); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function swap( + uint256 amount0Out, + uint256 amount1Out, + address to, + bytes calldata data + ) external; + + function burn(address to) + external + returns (uint256 amount0, uint256 amount1); + + function mint(address to) external returns (uint256 liquidity); + + function getReserves() + external + view + returns ( + uint256 _reserve0, + uint256 _reserve1, + uint256 _blockTimestampLast + ); + + function getAmountOut(uint256, address) external view returns (uint256); + + // ERC20 methods + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + function totalSupply() external view returns (uint256); + + function balanceOf(address) external view returns (uint256); + + function transfer(address recipient, uint256 amount) + external + returns (bool); + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + function allowance(address owner, address spender) + external + view + returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function claimable0(address _user) external view returns (uint256); + + function claimable1(address _user) external view returns (uint256); + + function isStable() external view returns (bool); + + function skim(address to) external; +} diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol new file mode 100644 index 0000000000..8aa5a1a2af --- /dev/null +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title SwapX Automated Market Maker (AMO) Strategy + * @notice AMO strategy for the SwapX OS/wS pool + * @author Origin Protocol Inc + */ +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { StableMath } from "../../utils/StableMath.sol"; +import { IVault } from "../../interfaces/IVault.sol"; +import { IWrappedSonic } from "../../interfaces/sonic/IWrappedSonic.sol"; +import { IPair } from "../../interfaces/sonic/ISwapXPair.sol"; +import { IGauge } from "../../interfaces/sonic/ISwapXGauge.sol"; + +contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { + using StableMath for uint256; + using SafeCast for uint256; + + /** + * @dev a threshold under which the contract no longer allows for the protocol to manually rebalance. + * Guarding against a strategist / guardian being taken over and with multiple transactions + * draining the protocol funds. + */ + uint256 public constant SOLVENCY_THRESHOLD = 0.998 ether; + + // New immutable variables that must be set in the constructor + /** + * @notice Address of the Wrapped S (wS) token. + */ + IWrappedSonic public immutable ws; + + /** + * @notice Address of the OS token contract. + */ + IERC20 public immutable os; + + /** + * @notice Address of the SwapX Stable pool contract. + */ + address public immutable pool; + + /** + * @notice Address of the SwapX Gauge contract. + */ + address public immutable gauge; + + /** + * @notice Maximum slippage allowed for adding/removing liquidity from the pool. + */ + uint256 public maxSlippage; + + event MaxSlippageUpdated(uint256 newMaxSlippage); + + /** + * @dev Verifies that the caller is the Strategist. + */ + modifier onlyStrategist() { + require( + msg.sender == IVault(vaultAddress).strategistAddr(), + "Caller is not the Strategist" + ); + _; + } + + constructor( + BaseStrategyConfig memory _baseConfig, + address _os, + address _ws, + address _gauge + ) InitializableAbstractStrategy(_baseConfig) { + os = IERC20(_os); + ws = IWrappedSonic(_ws); + + pool = _baseConfig.platformAddress; + gauge = _gauge; + + _setGovernor(address(0)); + } + + /** + * Initializer for setting up strategy internal state. This overrides the + * InitializableAbstractStrategy initializer as SwapX strategies don't fit + * well within that abstraction. + * @param _rewardTokenAddresses Address of CRV + * @param _maxSlippage Maximum slippage allowed for adding/removing liquidity from the pool. + */ + function initialize( + address[] calldata _rewardTokenAddresses, // CRV + uint256 _maxSlippage + ) external onlyGovernor initializer { + address[] memory pTokens = new address[](1); + pTokens[0] = pool; + + address[] memory _assets = new address[](1); + _assets[0] = address(ws); + + InitializableAbstractStrategy._initialize( + _rewardTokenAddresses, + _assets, + pTokens + ); + + _approveBase(); + _setMaxSlippage(_maxSlippage); + } + + /*************************************** + Deposit + ****************************************/ + + /** + * @notice Deposit Wrapped S (wS) into the SwapX pool + * @param _wS Address of Wrapped S (wS) contract. + * @param _amount Amount of Wrapped S (wS) to deposit. + */ + function deposit(address _wS, uint256 _amount) + external + override + onlyVault + nonReentrant + { + _deposit(_wS, _amount); + } + + function _deposit(address _wS, uint256 _wsAmount) internal { + require(_wsAmount > 0, "Must deposit something"); + require(_wS == address(ws), "Can only deposit wS"); + + emit Deposit(_wS, pool, _wsAmount); + + // Calculate the required amount of OS to mint based on the wS amount + // This ensure the proportion of OS tokens being added to the pool matches the proportion of wS tokens. + // For example, if the added wS tokens is 10% of existing wS tokens in the pool, + // then the OS tokens being added should also be 10% of the OS tokens in the pool. + (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); + uint256 osAmount = (_wsAmount * osReserves) / wsReserves; + + // Mint the required OS tokens to the strategy + IVault(vaultAddress).mintForStrategy(osAmount); + + // Transfer wS to the pool + ws.transfer(pool, _wsAmount); + // Transfer OS to the pool + os.transfer(pool, osAmount); + // Mint LP tokens from the pool + uint256 lpTokens = IPair(pool).mint(address(this)); + + // deposit the pool's LP tokens into the gauge + IGauge(gauge).deposit(lpTokens); + + emit Deposit(address(os), pool, osAmount); + + // Ensure solvency of the vault + _solvencyAssert(); + } + + /** + * @notice Deposit the strategy's entire balance of Wrapped S (wS) into the pool + */ + function depositAll() external override onlyVault nonReentrant { + uint256 balance = ws.balanceOf(address(this)); + if (balance > 0) { + _deposit(address(ws), balance); + } + } + + /*************************************** + Withdraw + ****************************************/ + + /** + * @notice Withdraw wS and OS from the SwapX pool, burn the OS, + * and transfer the wS to the recipient. + * @param _recipient Address to receive withdrawn asset which is normally the Vault. + * @param _ws Address of the Wrapped S (wS) contract. + * @param _wsAmount Amount of Wrapped S (wS) to withdraw. + */ + function withdraw( + address _recipient, + address _ws, + uint256 _wsAmount + ) external override onlyVault nonReentrant { + require(_wsAmount > 0, "Must withdraw something"); + require(_ws == address(ws), "Can only withdraw wS"); + + emit Withdrawal(_ws, pool, _wsAmount); + + // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back + uint256 lpTokens = calcTokensToBurn(_wsAmount); + + // Withdraw pool LP tokens from the gauge and remove assets from from the pool + _withdrawFromGaugeAndPool(lpTokens); + + // Burn all the removed OS and any that was left in the strategy + uint256 osToBurn = os.balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(osToBurn); + + emit Withdrawal(address(os), pool, osToBurn); + + // Transfer wS to the recipient + require( + ws.transfer(_recipient, _wsAmount), + "Transfer of wS not successful" + ); + + // Ensure solvency of the vault + _solvencyAssert(); + } + + /** + * @notice Remove all wS and OS from the SwapX pool, burn all the OS, + * and transfer all the wS to the Vault contract. + */ + function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); + // Can not withdraw zero LP tokens from the gauge + if (lpTokens == 0) return; + + // Withdraw pool LP tokens from the gauge and remove assets from from the pool + _withdrawFromGaugeAndPool(lpTokens); + + // Burn all OS + uint256 osToBurn = os.balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(osToBurn); + + // Get the strategy contract's wS balance. + // This includes all that was removed from the SwapX pool and + // any that was sitting in the strategy contract before the removal. + uint256 wsBalance = ws.balanceOf(address(this)); + require( + ws.transfer(vaultAddress, wsBalance), + "Transfer of wS not successful" + ); + + emit Withdrawal(address(ws), pool, wsBalance); + emit Withdrawal(address(os), pool, osToBurn); + } + + function calcTokensToBurn(uint256 _wsAmount) + internal + returns (uint256 lpTokens) + { + // Skim the pool in case extra wS tokens were added + IPair(pool).skim(address(this)); + + /* The rate between coins in the pool determines the rate at which pool returns + * tokens when doing balanced removal (remove_liquidity call). And by knowing how much wS + * we want we can determine how much of OS we receive by removing liquidity. + * + * Because we are doing balanced removal we should be making profit when removing liquidity in a + * pool tilted to either side. + * + * Important: A downside is that the Strategist / Governor needs to be + * cognizant of not removing too much liquidity. And while the proposal to remove liquidity + * is being voted on the pool tilt might change so much that the proposal that has been valid while + * created is no longer valid. + */ + + lpTokens = + ((_wsAmount + 1) * IPair(pool).totalSupply()) / + ws.balanceOf(pool); + } + + function _withdrawFromGaugeAndPool(uint256 lpTokens) internal { + // Withdraw pool LP tokens from the gauge + IGauge(gauge).withdraw(lpTokens); + + // Transfer the pool LP tokens to the pool + IPair(pool).transfer(pool, lpTokens); + // Burn the LP tokens and transfer the wS and OS back to the strategy + IPair(pool).burn(address(this)); + } + + /** + * Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can + * keep rebalancing the pool in both directions making the protocol lose a tiny amount of + * funds each time. + * + * Protocol must be at least SOLVENCY_THRESHOLD (99,8 %) backed in order for the rebalances to + * function. + */ + function _solvencyAssert() internal view { + uint256 _totalVaultValue = IVault(vaultAddress).totalValue(); + uint256 _totalOSSupply = os.totalSupply(); + + if ( + _totalVaultValue.divPrecisely(_totalOSSupply) < SOLVENCY_THRESHOLD + ) { + revert("Protocol insolvent"); + } + } + + /*************************************** + Assets and Rewards + ****************************************/ + + /** + * @notice Collect accumulated SWPx (and other) rewards and send to the Harvester. + */ + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + // Collect SWPx rewards from the gauge + IGauge(gauge).getReward(); + + _collectRewardTokens(); + } + + /** + * @notice Get the total asset value held in the SwapX pool + * @param _asset Address of the Wrapped S (wS) token + * @return balance Total value of the wS and OS tokens held in the pool + */ + function checkBalance(address _asset) + external + view + override + returns (uint256 balance) + { + require(_asset == address(ws), "Unsupported asset"); + + // wS balance needed here for the balance check that happens from vault during depositing. + balance = ws.balanceOf(address(this)); + + uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); + if (lpTokens == 0) return balance; + + // Add the strategy’s share of the wS and OS tokens in the SwapX pool. + // (pool’s wS reserves + pool’s OS reserves) * strategy’s LP tokens / total supply of pool LP tokens + (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); + balance += + ((wsReserves + osReserves) * lpTokens) / + IPair(pool).totalSupply(); + } + + /** + * @notice Returns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + */ + function supportsAsset(address _asset) public view override returns (bool) { + return _asset == address(ws); + } + + /*************************************** + Approvals + ****************************************/ + + /** + * @notice Sets the maximum slippage allowed for any swap/liquidity operation + * @param _maxSlippage Maximum slippage allowed, 1e18 = 100%. + */ + function setMaxSlippage(uint256 _maxSlippage) external onlyGovernor { + _setMaxSlippage(_maxSlippage); + } + + function _setMaxSlippage(uint256 _maxSlippage) internal { + require(_maxSlippage <= 5e16, "Slippage must be less than 100%"); + maxSlippage = _maxSlippage; + emit MaxSlippageUpdated(_maxSlippage); + } + + /** + * @notice Approve the spending of all assets by their corresponding pool tokens, + * if for some reason is it necessary. + */ + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + { + _approveBase(); + } + + // solhint-disable-next-line no-unused-vars + function _abstractSetPToken(address _asset, address _pToken) + internal + override + {} + + function _approveBase() internal { + // Approve pool for OS (required for adding liquidity) + // slither-disable-next-line unused-return + os.approve(platformAddress, type(uint256).max); + + // Approve SwapX pool for wS (required for adding liquidity) + // slither-disable-next-line unused-return + ws.approve(platformAddress, type(uint256).max); + + // Approve SwapX gauge contract to transfer SwapX pool LP tokens + // This is needed for deposits of SwapX pool LP tokens into the gauge. + // slither-disable-next-line unused-return + IPair(pool).approve(address(gauge), type(uint256).max); + } + + /** + * @dev Returns the largest of two numbers int256 version + */ + function _max(int256 a, int256 b) internal pure returns (int256) { + return a >= b ? a : b; + } +} From 85a2f9b1372de2f24ffd74f41f49e0647c232a0e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 20 Feb 2025 17:20:38 +1100 Subject: [PATCH 30/80] Added rebalancing functions --- .../sonic/SonicSwapXAMOStrategy.sol | 181 +++++++++++++++++- 1 file changed, 172 insertions(+), 9 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 8aa5a1a2af..600664fb56 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -54,6 +54,17 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { uint256 public maxSlippage; event MaxSlippageUpdated(uint256 newMaxSlippage); + event SwapOTokensToPool( + uint256 osToMint, + uint256 wsLiquidity, + uint256 osLiquidity, + uint256 lpTokens + ); + event SwapAssetsToPool( + uint256 _wsAmount, + uint256 lpTokens, + uint256 osToBurn + ); /** * @dev Verifies that the caller is the Strategist. @@ -66,6 +77,44 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { _; } + /** + * @dev Checks the pool's balances have improved and the balances + * have not tipped to the other side. + * This modifier is only applied to functions that do swaps against the pool. + * Deposits and withdrawals are proportional to the pool's balances hence don't need this check. + */ + modifier improvePoolBalance() { + // Get the asset and OToken balances in the pool + (uint256 wsReservesBefore, uint256 osReservesBefore, ) = IPair(pool) + .getReserves(); + // diff = wS balance - OS balance + int256 diffBefore = wsReservesBefore.toInt256() - + osReservesBefore.toInt256(); + + _; + + // Get the asset and OToken balances in the pool + (uint256 wsReservesAfter, uint256 osReservesAfter, ) = IPair(pool) + .getReserves(); + // diff = wS balance - OS balance + int256 diffAfter = wsReservesAfter.toInt256() - + osReservesAfter.toInt256(); + + if (diffBefore == 0) { + require(diffAfter == 0, "Position balance is worsened"); + } else if (diffBefore < 0) { + // If the pool was originally imbalanced in favor of OETH, then + // we want to check that the pool is now more balanced + require(diffAfter <= 0, "OTokens overshot peg"); + require(diffBefore < diffAfter, "OTokens balance worse"); + } else if (diffBefore > 0) { + // If the pool was originally imbalanced in favor of ETH, then + // we want to check that the pool is now more balanced + require(diffAfter >= 0, "Assets overshot peg"); + require(diffAfter < diffBefore, "Assets balance worse"); + } + } + constructor( BaseStrategyConfig memory _baseConfig, address _os, @@ -142,15 +191,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Mint the required OS tokens to the strategy IVault(vaultAddress).mintForStrategy(osAmount); - // Transfer wS to the pool - ws.transfer(pool, _wsAmount); - // Transfer OS to the pool - os.transfer(pool, osAmount); - // Mint LP tokens from the pool - uint256 lpTokens = IPair(pool).mint(address(this)); - - // deposit the pool's LP tokens into the gauge - IGauge(gauge).deposit(lpTokens); + // Add wS and OS liquidity to the pool and stake in gauge + _depositToPoolAndGauge(_wsAmount, osAmount); emit Deposit(address(os), pool, osAmount); @@ -240,6 +282,82 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { emit Withdrawal(address(os), pool, osToBurn); } + /*************************************** + Pool Rebalancing + ****************************************/ + + function swapAssetsToPool(uint256 _wsAmount) + external + onlyStrategist + nonReentrant + improvePoolBalance + { + // 1. Partially remove liquidity so there’s enough wS for the swap + + // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back + uint256 lpTokens = calcTokensToBurn(_wsAmount); + require(lpTokens > 0, "No LP tokens to burn"); + + _withdrawFromGaugeAndPool(lpTokens); + + // 2. Swap wS for OS against the pool + _swapExactTokensForTokens(_wsAmount, address(ws), address(os)); + + // 3. Burn all the OS left in the strategy from the remove liquidity and swap + uint256 osToBurn = os.balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(osToBurn); + + // Ensure solvency of the vault + _solvencyAssert(); + + // TODO Emit event with the _wsAmount, lpTokens and osToBurn + + emit SwapAssetsToPool(_wsAmount, lpTokens, osToBurn); + } + + function swapOTokensToPool(uint256 _osAmount) + external + onlyStrategist + nonReentrant + improvePoolBalance + { + // 1. Mint OS so it can be swapped into the pool + + // There shouldn't be any OS in the strategy but just in case + uint256 osInStrategy = os.balanceOf(address(this)); + require(_osAmount > osInStrategy, "OS in strategy"); + uint256 osToMint = _osAmount - osInStrategy; + + // Mint the required OS tokens to the strategy + IVault(vaultAddress).mintForStrategy(osToMint); + + // 2. Swap OS for wS against the pool + _swapExactTokensForTokens(_osAmount, address(os), address(ws)); + + // 3. Add wS and OS back to the pool in proportion to the pool's reserves + + // The wS is from the swap and any wS that was sitting in the strategy + uint256 wsLiquidity = ws.balanceOf(address(this)); + // Calculate how much OS liquidity is required from the wS liquidity + (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); + uint256 osLiquidity = (wsLiquidity * osReserves) / wsReserves; + + // Mint more OS so it can be added to the pool + IVault(vaultAddress).mintForStrategy(osLiquidity); + + // Add wS and OS liquidity to the pool and stake in gauge + uint256 lpTokens = _depositToPoolAndGauge(wsLiquidity, osLiquidity); + + // Ensure solvency of the vault + _solvencyAssert(); + + emit SwapOTokensToPool(osToMint, wsLiquidity, osLiquidity, lpTokens); + } + + /*************************************** + Pool Handling + ****************************************/ + function calcTokensToBurn(uint256 _wsAmount) internal returns (uint256 lpTokens) @@ -265,6 +383,22 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { ws.balanceOf(pool); } + function _depositToPoolAndGauge(uint256 _wsAmount, uint256 osAmount) + internal + returns (uint256 lpTokens) + { + // Transfer wS to the pool + ws.transfer(pool, _wsAmount); + // Transfer OS to the pool + os.transfer(pool, osAmount); + + // Mint LP tokens from the pool + lpTokens = IPair(pool).mint(address(this)); + + // Deposit the pool's LP tokens into the gauge + IGauge(gauge).deposit(lpTokens); + } + function _withdrawFromGaugeAndPool(uint256 lpTokens) internal { // Withdraw pool LP tokens from the gauge IGauge(gauge).withdraw(lpTokens); @@ -275,6 +409,35 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IPair(pool).burn(address(this)); } + function _swapExactTokensForTokens( + uint256 _amountIn, + address _tokenIn, + address _tokenOut + ) internal { + // Transfer in tokens to the pool + ws.transfer(pool, _amountIn); + + // Calculate how much out tokens we get from the swap + uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); + + // Safety check that we are dealing with the correct pool tokens + require( + (_tokenIn == address(ws) || _tokenIn == address(os)) && + (_tokenOut == address(ws) || _tokenOut == address(os)), + "Unsupported swap" + ); + + // Work out the correct order of the amounts for the pool + (uint256 amount0, uint256 amount1) = _tokenIn == address(ws) + ? (uint256(0), amountOut) + : (amountOut, 0); + + // Perform the swap on the pool + IPair(pool).swap(amount0, amount1, address(this), new bytes(0)); + + // TODO do we need a slippage check if the swap is profitable? + } + /** * Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can * keep rebalancing the pool in both directions making the protocol lose a tiny amount of From a7e11bd2e683a816ce341eafab9aba41b5228f5d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 20 Feb 2025 22:45:31 +1100 Subject: [PATCH 31/80] Added strategyValueChecker modifier to rebalance functions Removed maxSlippage --- .../sonic/SonicSwapXAMOStrategy.sol | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 600664fb56..16fd67e624 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -48,12 +48,6 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { */ address public immutable gauge; - /** - * @notice Maximum slippage allowed for adding/removing liquidity from the pool. - */ - uint256 public maxSlippage; - - event MaxSlippageUpdated(uint256 newMaxSlippage); event SwapOTokensToPool( uint256 osToMint, uint256 wsLiquidity, @@ -115,6 +109,20 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { } } + /// @dev Checks that the strategy value has not decreased by more than a dust amount + modifier strategyValueChecker() { + // Get the strategy value before the call + uint256 balanceBefore = checkBalance(address(ws)); + + _; + + // Get the strategy value after the call + uint256 balanceAfter = checkBalance(address(ws)); + + // The strategy value should not decrease by more than a dust amount + require(balanceAfter >= balanceBefore - 10, "Strategy value decreased"); + } + constructor( BaseStrategyConfig memory _baseConfig, address _os, @@ -135,11 +143,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * InitializableAbstractStrategy initializer as SwapX strategies don't fit * well within that abstraction. * @param _rewardTokenAddresses Address of CRV - * @param _maxSlippage Maximum slippage allowed for adding/removing liquidity from the pool. */ function initialize( - address[] calldata _rewardTokenAddresses, // CRV - uint256 _maxSlippage + address[] calldata _rewardTokenAddresses // CRV ) external onlyGovernor initializer { address[] memory pTokens = new address[](1); pTokens[0] = pool; @@ -154,7 +160,6 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { ); _approveBase(); - _setMaxSlippage(_maxSlippage); } /*************************************** @@ -286,11 +291,18 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { Pool Rebalancing ****************************************/ + /// @notice Used when there is more OS than wS in the pool. + /// wS and OS is removed from the pool, the received wS is swapped for OS + /// and the left over OS in the strategy is burnt. + /// The OS/wS price is < 1.0 so OS is being bought at a discount. + /// @param _wsAmount Amount of Wrapped S (wS) to swap into the pool. function swapAssetsToPool(uint256 _wsAmount) external onlyStrategist nonReentrant + // TODO which one is better to use here? improvePoolBalance + strategyValueChecker { // 1. Partially remove liquidity so there’s enough wS for the swap @@ -315,11 +327,17 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { emit SwapAssetsToPool(_wsAmount, lpTokens, osToBurn); } + /// @notice Used when there is more wS than OS in the pool. + /// OS is minted and swapped for wS against the pool, + /// more OS is minted and added back into the pool with the swapped out wS. + /// The OS/wS price is > 1.0 so OS is being sold at a premium. function swapOTokensToPool(uint256 _osAmount) external onlyStrategist nonReentrant + // TODO which one is better to use here? improvePoolBalance + strategyValueChecker { // 1. Mint OS so it can be swapped into the pool @@ -435,7 +453,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Perform the swap on the pool IPair(pool).swap(amount0, amount1, address(this), new bytes(0)); - // TODO do we need a slippage check if the swap is profitable? + // The slippage protection against the amount out is indirectly done via the improvePoolBalance and strategyValueChecker modifiers } /** @@ -482,7 +500,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * @return balance Total value of the wS and OS tokens held in the pool */ function checkBalance(address _asset) - external + public view override returns (uint256 balance) @@ -515,20 +533,6 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { Approvals ****************************************/ - /** - * @notice Sets the maximum slippage allowed for any swap/liquidity operation - * @param _maxSlippage Maximum slippage allowed, 1e18 = 100%. - */ - function setMaxSlippage(uint256 _maxSlippage) external onlyGovernor { - _setMaxSlippage(_maxSlippage); - } - - function _setMaxSlippage(uint256 _maxSlippage) internal { - require(_maxSlippage <= 5e16, "Slippage must be less than 100%"); - maxSlippage = _maxSlippage; - emit MaxSlippageUpdated(_maxSlippage); - } - /** * @notice Approve the spending of all assets by their corresponding pool tokens, * if for some reason is it necessary. From 8efda4af6a47524d9e6e8eb1d8c2ab8bbf483a3f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 20 Feb 2025 23:24:53 +1100 Subject: [PATCH 32/80] Added SwapX AMO deploy script --- contracts/contracts/proxies/SonicProxies.sol | 7 ++ contracts/deploy/sonic/010_swapx_amo.js | 97 ++++++++++++++++++++ contracts/utils/addresses.js | 6 ++ 3 files changed, 110 insertions(+) create mode 100644 contracts/deploy/sonic/010_swapx_amo.js diff --git a/contracts/contracts/proxies/SonicProxies.sol b/contracts/contracts/proxies/SonicProxies.sol index afce5699c7..d59d3a0477 100644 --- a/contracts/contracts/proxies/SonicProxies.sol +++ b/contracts/contracts/proxies/SonicProxies.sol @@ -51,3 +51,10 @@ contract OSonicHarvesterProxy is InitializeGovernedUpgradeabilityProxy { contract SonicCurveAMOStrategyProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice SonicSwapXAMOStrategyProxy delegates calls to a SonicSwapXAMOStrategy implementation + */ +contract SonicSwapXAMOStrategyProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/deploy/sonic/010_swapx_amo.js b/contracts/deploy/sonic/010_swapx_amo.js new file mode 100644 index 0000000000..2642f94fb0 --- /dev/null +++ b/contracts/deploy/sonic/010_swapx_amo.js @@ -0,0 +1,97 @@ +const { deployOnSonic } = require("../../utils/deploy-l2"); +const { + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); + +module.exports = deployOnSonic( + { + deployName: "010_swapx_amo", + }, + async ({ ethers }) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // Deploy Sonic SwapX AMO Strategy proxy + const cOSonicProxy = await ethers.getContract("OSonicProxy"); + const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); + const cOSonicVaultAdmin = await ethers.getContractAt( + "OSonicVaultAdmin", + cOSonicVaultProxy.address + ); + const cHarvesterProxy = await ethers.getContract("OSonicHarvesterProxy"); + const cHarvester = await ethers.getContractAt( + "OETHHarvesterSimple", + cHarvesterProxy.address + ); + + const dSonicSwapXAMOStrategyProxy = await deployWithConfirmation( + "SonicSwapXAMOStrategyProxy", + [] + ); + + const cSonicSwapXAMOStrategyProxy = await ethers.getContract( + "SonicSwapXAMOStrategyProxy" + ); + + // Deploy Sonic SwapX AMO Strategy implementation + const dSonicSwapXAMOStrategy = await deployWithConfirmation( + "SonicSwapXAMOStrategy", + [ + [addresses.sonic.SwapXWSOS.pool, cOSonicVaultProxy.address], + cOSonicProxy.address, + addresses.sonic.wS, + addresses.sonic.SwapXWSOS.gauge, + ] + ); + const cSonicSwapXAMOStrategy = await ethers.getContractAt( + "SonicSwapXAMOStrategy", + dSonicSwapXAMOStrategyProxy.address + ); + + // Initialize Sonic Curve AMO Strategy implementation + const initData = cSonicSwapXAMOStrategy.interface.encodeFunctionData( + "initialize(address[])", + [[addresses.sonic.SWPx]] + ); + await withConfirmation( + // prettier-ignore + cSonicSwapXAMOStrategyProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dSonicSwapXAMOStrategy.address, + addresses.sonic.timelock, + initData + ) + ); + + return { + actions: [ + // 1. Approve new strategy on the Vault + { + contract: cOSonicVaultAdmin, + signature: "approveStrategy(address)", + args: [cSonicSwapXAMOStrategyProxy.address], + }, + // 2. Add strategy to mint whitelist + { + contract: cOSonicVaultAdmin, + signature: "addStrategyToMintWhitelist(address)", + args: [cSonicSwapXAMOStrategyProxy.address], + }, + // 3. Enable for SwapX AMO after it has been deployed + { + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cSonicSwapXAMOStrategyProxy.address, true], + }, + // 4. Set the Harvester on the SwapX AMO strategy + { + contract: cSonicSwapXAMOStrategy, + signature: "setHarvesterAddress(address)", + args: [cHarvesterProxy.address], + }, + ], + }; + } +); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 6dd0e484a4..1fac616297 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -383,6 +383,7 @@ addresses.sonic.WOSonicProxy = "0x9F0dF7799f6FDAd409300080cfF680f5A23df4b1"; addresses.sonic.OSonicVaultProxy = "0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186"; // SwapX on Sonic +addresses.sonic.SWPx = "0xA04BC7140c26fc9BB1F36B1A604C7A5a88fb0E70"; addresses.sonic.SwapXSWPxOSPool = "0x9Cb484FAD38D953bc79e2a39bBc93655256F0B16"; addresses.sonic.SwapXTreasury = "0x896c3f0b63a8DAE60aFCE7Bca73356A9b611f3c8"; addresses.sonic.SwapXOsUSDCe = {}; @@ -401,6 +402,11 @@ addresses.sonic.SwapXOsGEMSx = {}; addresses.sonic.SwapXOsGEMSx.pool = "0x9ac7F5961a452e9cD5Be5717bD2c3dF412D1c1a5"; +addresses.sonic.SwapXWSOS = {}; +addresses.sonic.SwapXWSOS.pool = "0xcfe67b6c7b65c8d038e666b3241a161888b7f2b0"; +// TODO replace once the gauge is deployed +// addresses.sonic.SwapXWSOS.gauge = ; + addresses.sonic.SwapXOsUSDCeMultisigBooster = "0x4636269e7CDc253F6B0B210215C3601558FE80F6"; addresses.sonic.SwapXOsGEMSxMultisigBooster = From 8830c9cf03f3529ceebce4800d1c68e7af43b6ab Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 20 Feb 2025 23:26:12 +1100 Subject: [PATCH 33/80] Prettier --- contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 16fd67e624..84321370c4 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -453,7 +453,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Perform the swap on the pool IPair(pool).swap(amount0, amount1, address(this), new bytes(0)); - // The slippage protection against the amount out is indirectly done via the improvePoolBalance and strategyValueChecker modifiers + // The slippage protection against the amount out is indirectly done + // via the improvePoolBalance and strategyValueChecker modifiers } /** From d91f57447def35685a7bc392a3133ea54d44b37e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 25 Feb 2025 16:51:53 +1100 Subject: [PATCH 34/80] WIP creating the gauge in the deploy script --- .../contracts/interfaces/sonic/IVoterV3.sol | 15 +++++ .../{010_swapx_amo.js => 011_swapx_amo.js} | 55 +++++++++++++++++-- .../test/vault/vault.mainnet.fork-test.js | 4 +- contracts/utils/addresses.js | 4 +- 4 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 contracts/contracts/interfaces/sonic/IVoterV3.sol rename contracts/deploy/sonic/{010_swapx_amo.js => 011_swapx_amo.js} (65%) diff --git a/contracts/contracts/interfaces/sonic/IVoterV3.sol b/contracts/contracts/interfaces/sonic/IVoterV3.sol new file mode 100644 index 0000000000..451a274e7b --- /dev/null +++ b/contracts/contracts/interfaces/sonic/IVoterV3.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IVoterV3 { + /// @notice create a gauge + function createGauge(address _pool, uint256 _gaugeType) + external + returns ( + address _gauge, + address _internal_bribe, + address _external_bribe + ); + + function gauges(address _pool) external view returns (address _gauge); +} diff --git a/contracts/deploy/sonic/010_swapx_amo.js b/contracts/deploy/sonic/011_swapx_amo.js similarity index 65% rename from contracts/deploy/sonic/010_swapx_amo.js rename to contracts/deploy/sonic/011_swapx_amo.js index 2642f94fb0..e34ce2572b 100644 --- a/contracts/deploy/sonic/010_swapx_amo.js +++ b/contracts/deploy/sonic/011_swapx_amo.js @@ -4,15 +4,15 @@ const { withConfirmation, } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); +const { impersonateAndFund } = require("../../utils/signers"); module.exports = deployOnSonic( { - deployName: "010_swapx_amo", + deployName: "011_swapx_amo", }, async ({ ethers }) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - // Deploy Sonic SwapX AMO Strategy proxy const cOSonicProxy = await ethers.getContract("OSonicProxy"); const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); @@ -25,16 +25,61 @@ module.exports = deployOnSonic( "OETHHarvesterSimple", cHarvesterProxy.address ); - const dSonicSwapXAMOStrategyProxy = await deployWithConfirmation( "SonicSwapXAMOStrategyProxy", [] ); - const cSonicSwapXAMOStrategyProxy = await ethers.getContract( "SonicSwapXAMOStrategyProxy" ); + console.log( + `Getting reference to swapXVoter ${addresses.sonic.SwapXVoter}` + ); + const swapXVoter = await ethers.getContractAt( + "IVoterV3", + addresses.sonic.SwapXVoter + ); + + console.log(`About to get gauge for the wS/OS pool`); + const gaugeAddress = await swapXVoter.gauges( + addresses.sonic.SwapXWSOS.pool + ); + console.log( + `Gauge for the wS/OS pool ${ + addresses.sonic.SwapXWSOS.pool + } is ${await swapXVoter.gauges(addresses.sonic.SwapXWSOS.pool)}` + ); + + if (gaugeAddress === addresses.zero) { + console.log(`Getting SwapX owner signer ${addresses.sonic.SwapXOwner}`); + // Create the wS/OS Gauge + const swapXOwnerSigner = await impersonateAndFund( + addresses.sonic.SwapXOwner + ); + + console.log( + `Creating gauge for the wS/OS pool ${ + addresses.sonic.SwapXWSOS.pool + } using signer ${await swapXOwnerSigner.getAddress()}` + ); + const tx = await swapXVoter + .connect(swapXOwnerSigner) + .createGauge(addresses.sonic.SwapXWSOS.pool, 0); + + console.log(`Waiting for the createGauge tx ${tx.hash}`); + + const receipt = await tx.wait(); + const createGaugeEvent = receipt.events.find( + (e) => e.event === "GaugeCreated" + ); + + console.log(`Gauge created: ${createGaugeEvent.args.gauge}`); + addresses.sonic.SwapXWSOS.gauge = createGaugeEvent.args.gauge; + } else { + addresses.sonic.SwapXWSOS.gauge = gaugeAddress; + } + // Deploy Sonic SwapX AMO Strategy implementation const dSonicSwapXAMOStrategy = await deployWithConfirmation( "SonicSwapXAMOStrategy", @@ -49,7 +94,6 @@ module.exports = deployOnSonic( "SonicSwapXAMOStrategy", dSonicSwapXAMOStrategyProxy.address ); - // Initialize Sonic Curve AMO Strategy implementation const initData = cSonicSwapXAMOStrategy.interface.encodeFunctionData( "initialize(address[])", @@ -64,7 +108,6 @@ module.exports = deployOnSonic( initData ) ); - return { actions: [ // 1. Approve new strategy on the Vault diff --git a/contracts/test/vault/vault.mainnet.fork-test.js b/contracts/test/vault/vault.mainnet.fork-test.js index 66338b7e2a..89c3ef9121 100644 --- a/contracts/test/vault/vault.mainnet.fork-test.js +++ b/contracts/test/vault/vault.mainnet.fork-test.js @@ -13,7 +13,9 @@ const { isCI, } = require("./../helpers"); const { impersonateAndFund } = require("../../utils/signers"); -const { shouldHaveRewardTokensConfigured } = require("./../behaviour/reward-tokens.fork"); +const { + shouldHaveRewardTokensConfigured, +} = require("./../behaviour/reward-tokens.fork"); const log = require("../../utils/logger")("test:fork:ousd:vault"); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 1fac616297..7ffc65ec85 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -384,6 +384,8 @@ addresses.sonic.OSonicVaultProxy = "0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186"; // SwapX on Sonic addresses.sonic.SWPx = "0xA04BC7140c26fc9BB1F36B1A604C7A5a88fb0E70"; +addresses.sonic.SwapXOwner = "0xAdB5A1518713095C39dBcA08Da6656af7249Dd20"; +addresses.sonic.SwapXVoter = "0xC1AE2779903cfB84CB9DEe5c03EcEAc32dc407F2"; addresses.sonic.SwapXSWPxOSPool = "0x9Cb484FAD38D953bc79e2a39bBc93655256F0B16"; addresses.sonic.SwapXTreasury = "0x896c3f0b63a8DAE60aFCE7Bca73356A9b611f3c8"; addresses.sonic.SwapXOsUSDCe = {}; @@ -405,7 +407,7 @@ addresses.sonic.SwapXOsGEMSx.pool = addresses.sonic.SwapXWSOS = {}; addresses.sonic.SwapXWSOS.pool = "0xcfe67b6c7b65c8d038e666b3241a161888b7f2b0"; // TODO replace once the gauge is deployed -// addresses.sonic.SwapXWSOS.gauge = ; +// addresses.sonic.SwapXWSOS.gauge = undefined; addresses.sonic.SwapXOsUSDCeMultisigBooster = "0x4636269e7CDc253F6B0B210215C3601558FE80F6"; From 7ea30afa9ba3a76e6387c55eeda2ff2af83cb985 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 25 Feb 2025 17:59:46 +1100 Subject: [PATCH 35/80] Removed most of the logging --- contracts/deploy/sonic/011_swapx_amo.js | 17 +---------------- contracts/utils/addresses.js | 2 +- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/contracts/deploy/sonic/011_swapx_amo.js b/contracts/deploy/sonic/011_swapx_amo.js index e34ce2572b..89d92cb012 100644 --- a/contracts/deploy/sonic/011_swapx_amo.js +++ b/contracts/deploy/sonic/011_swapx_amo.js @@ -33,15 +33,11 @@ module.exports = deployOnSonic( "SonicSwapXAMOStrategyProxy" ); - console.log( - `Getting reference to swapXVoter ${addresses.sonic.SwapXVoter}` - ); const swapXVoter = await ethers.getContractAt( "IVoterV3", addresses.sonic.SwapXVoter ); - console.log(`About to get gauge for the wS/OS pool`); const gaugeAddress = await swapXVoter.gauges( addresses.sonic.SwapXWSOS.pool ); @@ -52,7 +48,6 @@ module.exports = deployOnSonic( ); if (gaugeAddress === addresses.zero) { - console.log(`Getting SwapX owner signer ${addresses.sonic.SwapXOwner}`); // Create the wS/OS Gauge const swapXOwnerSigner = await impersonateAndFund( addresses.sonic.SwapXOwner @@ -63,19 +58,9 @@ module.exports = deployOnSonic( addresses.sonic.SwapXWSOS.pool } using signer ${await swapXOwnerSigner.getAddress()}` ); - const tx = await swapXVoter + await swapXVoter .connect(swapXOwnerSigner) .createGauge(addresses.sonic.SwapXWSOS.pool, 0); - - console.log(`Waiting for the createGauge tx ${tx.hash}`); - - const receipt = await tx.wait(); - const createGaugeEvent = receipt.events.find( - (e) => e.event === "GaugeCreated" - ); - - console.log(`Gauge created: ${createGaugeEvent.args.gauge}`); - addresses.sonic.SwapXWSOS.gauge = createGaugeEvent.args.gauge; } else { addresses.sonic.SwapXWSOS.gauge = gaugeAddress; } diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 7ffc65ec85..8f4d23c368 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -407,7 +407,7 @@ addresses.sonic.SwapXOsGEMSx.pool = addresses.sonic.SwapXWSOS = {}; addresses.sonic.SwapXWSOS.pool = "0xcfe67b6c7b65c8d038e666b3241a161888b7f2b0"; // TODO replace once the gauge is deployed -// addresses.sonic.SwapXWSOS.gauge = undefined; +addresses.sonic.SwapXWSOS.gauge = "0x2e5228cfecfefa439f92aa4823e5593de5bfcb7d"; addresses.sonic.SwapXOsUSDCeMultisigBooster = "0x4636269e7CDc253F6B0B210215C3601558FE80F6"; From 0f1ff278e9b94e0c31c7e390c5efd0c07a78dc9d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 26 Feb 2025 09:59:06 +1100 Subject: [PATCH 36/80] Started SwapX AMO fork tests --- .../contracts/interfaces/sonic/ISwapXPair.sol | 3 + .../sonic/SonicSwapXAMOStrategy.sol | 76 +- contracts/test/_fixture-sonic.js | 130 ++- .../sonic/swapx-amo.sonic.fork-test.js | 842 ++++++++++++++++++ contracts/utils/addresses.js | 4 +- 5 files changed, 995 insertions(+), 60 deletions(-) create mode 100644 contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js diff --git a/contracts/contracts/interfaces/sonic/ISwapXPair.sol b/contracts/contracts/interfaces/sonic/ISwapXPair.sol index ce245bb121..b4a7ea5f4c 100644 --- a/contracts/contracts/interfaces/sonic/ISwapXPair.sol +++ b/contracts/contracts/interfaces/sonic/ISwapXPair.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.0; interface IPair { + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + function metadata() external view diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 84321370c4..73abe02c11 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -31,12 +31,12 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { /** * @notice Address of the Wrapped S (wS) token. */ - IWrappedSonic public immutable ws; + address public immutable ws; /** * @notice Address of the OS token contract. */ - IERC20 public immutable os; + address public immutable os; /** * @notice Address of the SwapX Stable pool contract. @@ -112,12 +112,12 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { /// @dev Checks that the strategy value has not decreased by more than a dust amount modifier strategyValueChecker() { // Get the strategy value before the call - uint256 balanceBefore = checkBalance(address(ws)); + uint256 balanceBefore = checkBalance(ws); _; // Get the strategy value after the call - uint256 balanceAfter = checkBalance(address(ws)); + uint256 balanceAfter = checkBalance(ws); // The strategy value should not decrease by more than a dust amount require(balanceAfter >= balanceBefore - 10, "Strategy value decreased"); @@ -129,8 +129,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { address _ws, address _gauge ) InitializableAbstractStrategy(_baseConfig) { - os = IERC20(_os); - ws = IWrappedSonic(_ws); + os = _os; + ws = _ws; pool = _baseConfig.platformAddress; gauge = _gauge; @@ -151,7 +151,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { pTokens[0] = pool; address[] memory _assets = new address[](1); - _assets[0] = address(ws); + _assets[0] = ws; InitializableAbstractStrategy._initialize( _rewardTokenAddresses, @@ -182,7 +182,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { function _deposit(address _wS, uint256 _wsAmount) internal { require(_wsAmount > 0, "Must deposit something"); - require(_wS == address(ws), "Can only deposit wS"); + require(_wS == ws, "Can only deposit wS"); emit Deposit(_wS, pool, _wsAmount); @@ -199,7 +199,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Add wS and OS liquidity to the pool and stake in gauge _depositToPoolAndGauge(_wsAmount, osAmount); - emit Deposit(address(os), pool, osAmount); + emit Deposit(os, pool, osAmount); // Ensure solvency of the vault _solvencyAssert(); @@ -209,9 +209,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * @notice Deposit the strategy's entire balance of Wrapped S (wS) into the pool */ function depositAll() external override onlyVault nonReentrant { - uint256 balance = ws.balanceOf(address(this)); + uint256 balance = IWrappedSonic(ws).balanceOf(address(this)); if (balance > 0) { - _deposit(address(ws), balance); + _deposit(ws, balance); } } @@ -232,7 +232,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { uint256 _wsAmount ) external override onlyVault nonReentrant { require(_wsAmount > 0, "Must withdraw something"); - require(_ws == address(ws), "Can only withdraw wS"); + require(_ws == ws, "Can only withdraw wS"); emit Withdrawal(_ws, pool, _wsAmount); @@ -243,14 +243,14 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { _withdrawFromGaugeAndPool(lpTokens); // Burn all the removed OS and any that was left in the strategy - uint256 osToBurn = os.balanceOf(address(this)); + uint256 osToBurn = IERC20(os).balanceOf(address(this)); IVault(vaultAddress).burnForStrategy(osToBurn); - emit Withdrawal(address(os), pool, osToBurn); + emit Withdrawal(os, pool, osToBurn); // Transfer wS to the recipient require( - ws.transfer(_recipient, _wsAmount), + IWrappedSonic(ws).transfer(_recipient, _wsAmount), "Transfer of wS not successful" ); @@ -271,20 +271,20 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { _withdrawFromGaugeAndPool(lpTokens); // Burn all OS - uint256 osToBurn = os.balanceOf(address(this)); + uint256 osToBurn = IERC20(os).balanceOf(address(this)); IVault(vaultAddress).burnForStrategy(osToBurn); // Get the strategy contract's wS balance. // This includes all that was removed from the SwapX pool and // any that was sitting in the strategy contract before the removal. - uint256 wsBalance = ws.balanceOf(address(this)); + uint256 wsBalance = IWrappedSonic(ws).balanceOf(address(this)); require( - ws.transfer(vaultAddress, wsBalance), + IWrappedSonic(ws).transfer(vaultAddress, wsBalance), "Transfer of wS not successful" ); - emit Withdrawal(address(ws), pool, wsBalance); - emit Withdrawal(address(os), pool, osToBurn); + emit Withdrawal(ws, pool, wsBalance); + emit Withdrawal(os, pool, osToBurn); } /*************************************** @@ -313,10 +313,10 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { _withdrawFromGaugeAndPool(lpTokens); // 2. Swap wS for OS against the pool - _swapExactTokensForTokens(_wsAmount, address(ws), address(os)); + _swapExactTokensForTokens(_wsAmount, ws, os); // 3. Burn all the OS left in the strategy from the remove liquidity and swap - uint256 osToBurn = os.balanceOf(address(this)); + uint256 osToBurn = IERC20(os).balanceOf(address(this)); IVault(vaultAddress).burnForStrategy(osToBurn); // Ensure solvency of the vault @@ -342,7 +342,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // 1. Mint OS so it can be swapped into the pool // There shouldn't be any OS in the strategy but just in case - uint256 osInStrategy = os.balanceOf(address(this)); + uint256 osInStrategy = IERC20(os).balanceOf(address(this)); require(_osAmount > osInStrategy, "OS in strategy"); uint256 osToMint = _osAmount - osInStrategy; @@ -350,12 +350,12 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IVault(vaultAddress).mintForStrategy(osToMint); // 2. Swap OS for wS against the pool - _swapExactTokensForTokens(_osAmount, address(os), address(ws)); + _swapExactTokensForTokens(_osAmount, os, ws); // 3. Add wS and OS back to the pool in proportion to the pool's reserves // The wS is from the swap and any wS that was sitting in the strategy - uint256 wsLiquidity = ws.balanceOf(address(this)); + uint256 wsLiquidity = IWrappedSonic(ws).balanceOf(address(this)); // Calculate how much OS liquidity is required from the wS liquidity (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); uint256 osLiquidity = (wsLiquidity * osReserves) / wsReserves; @@ -398,7 +398,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { lpTokens = ((_wsAmount + 1) * IPair(pool).totalSupply()) / - ws.balanceOf(pool); + IWrappedSonic(ws).balanceOf(pool); } function _depositToPoolAndGauge(uint256 _wsAmount, uint256 osAmount) @@ -406,9 +406,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { returns (uint256 lpTokens) { // Transfer wS to the pool - ws.transfer(pool, _wsAmount); + IWrappedSonic(ws).transfer(pool, _wsAmount); // Transfer OS to the pool - os.transfer(pool, osAmount); + IERC20(os).transfer(pool, osAmount); // Mint LP tokens from the pool lpTokens = IPair(pool).mint(address(this)); @@ -433,20 +433,20 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { address _tokenOut ) internal { // Transfer in tokens to the pool - ws.transfer(pool, _amountIn); + IWrappedSonic(ws).transfer(pool, _amountIn); // Calculate how much out tokens we get from the swap uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); // Safety check that we are dealing with the correct pool tokens require( - (_tokenIn == address(ws) || _tokenIn == address(os)) && - (_tokenOut == address(ws) || _tokenOut == address(os)), + (_tokenIn == ws || _tokenIn == os) && + (_tokenOut == ws || _tokenOut == os), "Unsupported swap" ); // Work out the correct order of the amounts for the pool - (uint256 amount0, uint256 amount1) = _tokenIn == address(ws) + (uint256 amount0, uint256 amount1) = _tokenIn == ws ? (uint256(0), amountOut) : (amountOut, 0); @@ -467,7 +467,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { */ function _solvencyAssert() internal view { uint256 _totalVaultValue = IVault(vaultAddress).totalValue(); - uint256 _totalOSSupply = os.totalSupply(); + uint256 _totalOSSupply = IERC20(os).totalSupply(); if ( _totalVaultValue.divPrecisely(_totalOSSupply) < SOLVENCY_THRESHOLD @@ -506,10 +506,10 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { override returns (uint256 balance) { - require(_asset == address(ws), "Unsupported asset"); + require(_asset == ws, "Unsupported asset"); // wS balance needed here for the balance check that happens from vault during depositing. - balance = ws.balanceOf(address(this)); + balance = IWrappedSonic(ws).balanceOf(address(this)); uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); if (lpTokens == 0) return balance; @@ -527,7 +527,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * @param _asset Address of the asset */ function supportsAsset(address _asset) public view override returns (bool) { - return _asset == address(ws); + return _asset == ws; } /*************************************** @@ -556,11 +556,11 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { function _approveBase() internal { // Approve pool for OS (required for adding liquidity) // slither-disable-next-line unused-return - os.approve(platformAddress, type(uint256).max); + IERC20(os).approve(platformAddress, type(uint256).max); // Approve SwapX pool for wS (required for adding liquidity) // slither-disable-next-line unused-return - ws.approve(platformAddress, type(uint256).max); + IWrappedSonic(ws).approve(platformAddress, type(uint256).max); // Approve SwapX gauge contract to transfer SwapX pool LP tokens // This is needed for deposits of SwapX pool LP tokens into the gauge. diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 478dd875ba..5a90267819 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -1,12 +1,14 @@ const hre = require("hardhat"); const { ethers } = hre; +const { parseUnits } = ethers.utils; const mocha = require("mocha"); +const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); + const { isFork, isSonicFork, oethUnits } = require("./helpers"); const { deployWithConfirmation } = require("../utils/deploy.js"); const { impersonateAndFund } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); const addresses = require("../utils/addresses"); -const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); const erc20Abi = require("./abi/erc20.json"); const curveXChainLiquidityGaugeAbi = require("./abi/curveXChainLiquidityGauge.json"); @@ -150,7 +152,15 @@ const defaultSonicFixture = deployments.createFixture(async () => { const oSonicVaultSigner = await impersonateAndFund(oSonicVault.address); - let validatorRegistrator, curveAMOStrategy; + let validatorRegistrator, + curveAMOStrategy, + curvePool, + curveGauge, + curveChildLiquidityGaugeFactory, + crv, + swapXAMOStrategy, + swapXPool, + swapXGauge; if (isFork) { validatorRegistrator = await impersonateAndFund( addresses.sonic.validatorRegistrator @@ -159,7 +169,7 @@ const defaultSonicFixture = deployments.createFixture(async () => { await sonicStakingStrategy.connect(strategist).setDefaultValidatorId(18); - // Curve + // Curve AMO const curveAMOProxy = await ethers.getContract( "SonicCurveAMOStrategyProxy" ); @@ -167,6 +177,42 @@ const defaultSonicFixture = deployments.createFixture(async () => { "SonicCurveAMOStrategy", curveAMOProxy.address ); + + curvePool = await ethers.getContractAt( + curveStableSwapNGAbi, + addresses.sonic.WS_OS.pool + ); + + curveGauge = await ethers.getContractAt( + curveXChainLiquidityGaugeAbi, + addresses.sonic.WS_OS.gauge + ); + + curveChildLiquidityGaugeFactory = await ethers.getContractAt( + curveChildLiquidityGaugeFactoryAbi, + addresses.sonic.childLiquidityGaugeFactory + ); + + crv = await ethers.getContractAt(erc20Abi, addresses.sonic.CRV); + + // SwapX AMO + const swapXAMOProxy = await ethers.getContract( + "SonicSwapXAMOStrategyProxy" + ); + swapXAMOStrategy = await ethers.getContractAt( + "SonicSwapXAMOStrategy", + swapXAMOProxy.address + ); + + swapXPool = await ethers.getContractAt( + "IPair", + addresses.sonic.SwapXWSOS.pool + ); + + swapXGauge = await ethers.getContractAt( + "IGauge", + addresses.sonic.SwapXWSOS.gauge + ); } for (const user of [rafael, nick, clement]) { @@ -178,23 +224,6 @@ const defaultSonicFixture = deployments.createFixture(async () => { await wS.connect(user).approve(oSonicVault.address, oethUnits("5000")); } - const curvePool = await ethers.getContractAt( - curveStableSwapNGAbi, - addresses.sonic.WS_OS.pool - ); - - const curveGauge = await ethers.getContractAt( - curveXChainLiquidityGaugeAbi, - addresses.sonic.WS_OS.gauge - ); - - const curveChildLiquidityGaugeFactory = await ethers.getContractAt( - curveChildLiquidityGaugeFactoryAbi, - addresses.sonic.childLiquidityGaugeFactory - ); - - const crv = await ethers.getContractAt(erc20Abi, addresses.sonic.CRV); - return { // Origin S oSonic, @@ -218,6 +247,11 @@ const defaultSonicFixture = deployments.createFixture(async () => { curveChildLiquidityGaugeFactory, crv, + // SwapX + swapXAMOStrategy, + swapXPool, + swapXGauge, + // Signers governor, strategist, @@ -236,6 +270,61 @@ const defaultSonicFixture = deployments.createFixture(async () => { }; }); +/** + * Configure a Vault with only the OETH/(W)ETH Curve Metastrategy. + */ +async function swapXAMOFixture( + config = { + wsMintAmount: 0, + depositToStrategy: false, + poolAddwSAmount: 0, + poolAddOSAmount: 0, + balancePool: false, + } +) { + const fixture = await defaultSonicFixture(); + + const { oSonicVault, nick, strategist, swapXAMOStrategy, timelock, wS } = + fixture; + + await oSonicVault + .connect(timelock) + .setAssetDefaultStrategy(wS.address, addresses.zero); + + // mint some OS using wS if configured + if (config?.wsMintAmount > 0) { + const wsAmount = parseUnits(config.wsMintAmount.toString()); + await oSonicVault.connect(nick).rebase(); + await oSonicVault.connect(nick).allocate(); + + // Calculate how much to mint based on the wS in the vault, + // the withdrawal queue, and the wS to be sent to the strategy + const wsBalance = await wS.balanceOf(oSonicVault.address); + const queue = await oSonicVault.withdrawalQueueMetadata(); + const available = wsBalance.add(queue.claimed).sub(queue.queued); + const mintAmount = wsAmount.sub(available); + + if (mintAmount.gt(0)) { + // Approve the Vault to transfer wS + await wS.connect(nick).approve(oSonicVault.address, mintAmount); + + // Mint OS with wS + // This will sit in the vault, not the strategy + await oSonicVault.connect(nick).mint(wS.address, mintAmount, 0); + } + + // Add ETH to the Metapool + if (config?.depositToStrategy) { + // The strategist deposits the WETH to the AMO strategy + await oSonicVault + .connect(strategist) + .depositToStrategy(swapXAMOStrategy.address, [wS.address], [wsAmount]); + } + } + + return fixture; +} + const deployPoolBoosterFactorySwapxSingle = async ( poolBoosterCentralRegistry, governor @@ -270,6 +359,7 @@ mocha.after(async () => { module.exports = { defaultSonicFixture, + swapXAMOFixture, MINTER_ROLE, BURNER_ROLE, }; diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js new file mode 100644 index 0000000000..bd834a8d08 --- /dev/null +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -0,0 +1,842 @@ +const { expect } = require("chai"); +const { formatUnits, parseUnits } = require("ethers/lib/utils"); +const { run } = require("hardhat"); + +const { createFixtureLoader } = require("../../_fixture"); +const { + defaultSonicFixture, + swapXAMOFixture, +} = require("../../_fixture-sonic"); +const { units, oethUnits, isCI } = require("../../helpers"); +const addresses = require("../../../utils/addresses"); + +const log = require("../../../utils/logger")("test:fork:sonic:swapx:amo"); + +describe("Sonic ForkTest: SwapX AMO Strategy", function () { + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture; + + describe("post deployment", () => { + const loadFixture = createFixtureLoader(defaultSonicFixture); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Should have constants and immutables set", async () => { + const { swapXAMOStrategy } = fixture; + + expect(await swapXAMOStrategy.SOLVENCY_THRESHOLD()).to.equal( + parseUnits("0.998", 18) + ); + expect(await swapXAMOStrategy.ws()).to.equal(addresses.sonic.wS); + expect(await swapXAMOStrategy.os()).to.equal(addresses.sonic.OSonicProxy); + expect(await swapXAMOStrategy.pool()).to.equal( + addresses.sonic.SwapXWSOS.pool + ); + expect(await swapXAMOStrategy.gauge()).to.equal( + addresses.sonic.SwapXWSOS.gauge + ); + expect(await swapXAMOStrategy.governor()).to.equal( + addresses.sonic.timelock + ); + expect(await swapXAMOStrategy.supportsAsset(addresses.sonic.wS)).to.true; + }); + it("Should be able to check balance", async () => { + const { wS, nick, swapXAMOStrategy } = fixture; + + const balance = await swapXAMOStrategy.checkBalance(wS.address); + log(`check balance ${balance}`); + expect(balance).gte(0); + + // This uses a transaction to call a view function so the gas usage can be reported. + const tx = await swapXAMOStrategy + .connect(nick) + .populateTransaction.checkBalance(wS.address); + await nick.sendTransaction(tx); + }); + it("Only Governor can approve all tokens", async () => { + const { + timelock, + strategist, + nick, + oSonicVaultSigner, + swapXAMOStrategy, + wS, + oSonic, + swapXPool, + } = fixture; + + expect(await swapXAMOStrategy.connect(timelock).isGovernor()).to.equal( + true + ); + + // Timelock can approve all tokens + const tx = await swapXAMOStrategy + .connect(timelock) + .safeApproveAllTokens(); + await expect(tx).to.emit(wS, "Approval"); + await expect(tx).to.emit(oSonic, "Approval"); + await expect(tx).to.emit(swapXPool, "Approval"); + + for (const signer of [strategist, nick, oSonicVaultSigner]) { + const tx = swapXAMOStrategy.connect(signer).safeApproveAllTokens(); + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + } + }); + }); + + describe.skip("with some wS in the vault", () => { + const loadFixture = createFixtureLoader(swapXAMOFixture, { + wsMintAmount: 5000, + depositToStrategy: false, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Vault should deposit some wS to AMO strategy", async function () { + const { swapXAMOStrategy, oSonic, swapXPool, oSonicVaultSigner, wS } = + fixture; + + const wsDepositAmount = await units("5000", wS); + + // Vault transfers WETH to strategy + await wS + .connect(oSonicVaultSigner) + .transfer(swapXAMOStrategy.address, wsDepositAmount); + + const { osMintAmount, curveBalances: curveBalancesBefore } = + await calcOSMintAmount(fixture, wsDepositAmount); + const osSupplyBefore = await oSonic.totalSupply(); + + // log("Before deposit to strategy"); + // await run("amoStrat", { + // pool: "OS", + // output: false, + // }); + + const tx = await swapXAMOStrategy + .connect(oSonicVaultSigner) + .deposit(wS.address, wsDepositAmount); + + // const receipt = await tx.wait(); + + // log("After deposit to strategy"); + // await run("amoStrat", { + // pool: "OS", + // output: false, + // fromBlock: receipt.blockNumber - 1, + // }); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(wS.address, swapXPool.address, wsDepositAmount); + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(oSonic.address, swapXPool.address, osMintAmount); + + // Check the ETH and OS balances in the Curve pool + const curveBalancesAfter = await swapXPool.get_balances(); + expect(curveBalancesAfter[0]).to.approxEqualTolerance( + curveBalancesBefore[0].add(wsDepositAmount), + 0.01 // 0.01% or 1 basis point + ); + expect(curveBalancesAfter[1]).to.approxEqualTolerance( + curveBalancesBefore[1].add(osMintAmount), + 0.01 // 0.01% or 1 basis point + ); + + // Check the OS total supply increase + const oethSupplyAfter = await oSonic.totalSupply(); + expect(oethSupplyAfter).to.approxEqualTolerance( + osSupplyBefore.add(osMintAmount), + 0.01 // 0.01% or 1 basis point + ); + }); + it("Only vault can deposit some wS to AMO strategy", async function () { + const { + swapXAMOStrategy, + oSonicVaultSigner, + strategist, + timelock, + nick, + wS, + } = fixture; + + const depositAmount = parseUnits("50"); + await wS + .connect(oSonicVaultSigner) + .transfer(swapXAMOStrategy.address, depositAmount); + + for (const signer of [strategist, timelock, nick]) { + const tx = swapXAMOStrategy + .connect(signer) + .deposit(wS.address, depositAmount); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + }); + it("Only vault can deposit all wS to AMO strategy", async function () { + const { + swapXAMOStrategy, + swapXPool, + oSonicVaultSigner, + strategist, + timelock, + nick, + wS, + } = fixture; + + const depositAmount = parseUnits("50"); + await wS + .connect(oSonicVaultSigner) + .transfer(swapXAMOStrategy.address, depositAmount); + + for (const signer of [strategist, timelock, nick]) { + const tx = swapXAMOStrategy.connect(signer).depositAll(); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + + const tx = await swapXAMOStrategy.connect(oSonicVaultSigner).depositAll(); + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withNamedArgs({ _asset: wS.address, _pToken: swapXPool.address }); + }); + }); + + describe.skip("with the strategy having some OS and wS in the pool", () => { + const loadFixture = createFixtureLoader(defaultSonicFixture, { + wsMintAmount: 5000, + depositToStrategy: true, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Vault should be able to withdraw all", async () => { + const { swapXAMOStrategy, swapXPool, oSonic, oSonicVaultSigner, wS } = + fixture; + + const { + oethBurnAmount, + ethWithdrawAmount, + curveBalances: curveBalancesBefore, + } = await calcWithdrawAllAmounts(fixture); + + const oethSupplyBefore = await oSonic.totalSupply(); + + log("Before withdraw all from strategy"); + await run("amoStrat", { + pool: "OS", + output: false, + }); + + // Now try to withdraw all the WETH from the strategy + const tx = await swapXAMOStrategy + .connect(oSonicVaultSigner) + .withdrawAll(); + + const receipt = await tx.wait(); + + log("After withdraw all from strategy"); + await run("amoStrat", { + pool: "OS", + output: false, + fromBlock: receipt.blockNumber - 1, + }); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Withdrawal") + .withArgs(wS.address, swapXPool.address, ethWithdrawAmount); + await expect(tx) + .to.emit(swapXAMOStrategy, "Withdrawal") + .withArgs(oSonic.address, swapXPool.address, oethBurnAmount); + + // Check the ETH and OS balances in the Curve pool + const curveBalancesAfter = await swapXPool.get_balances(); + expect(curveBalancesAfter[0]).to.approxEqualTolerance( + curveBalancesBefore[0].sub(ethWithdrawAmount), + 0.05 // 0.05% or 5 basis point + ); + expect(curveBalancesAfter[1]).to.approxEqualTolerance( + curveBalancesBefore[1].sub(oethBurnAmount), + 0.05 // 0.05% + ); + + // Check the OS total supply decrease + const oethSupplyAfter = await oSonic.totalSupply(); + expect(oethSupplyAfter).to.approxEqualTolerance( + oethSupplyBefore.sub(oethBurnAmount), + 0.05 // 0.01% or 5 basis point + ); + }); + it("Vault should be able to withdraw some", async () => { + const { + swapXAMOStrategy, + oSonic, + swapXPool, + oethVault, + oSonicVaultSigner, + wS, + } = fixture; + + const withdrawAmount = oethUnits("1000"); + + const { oethBurnAmount, curveBalances: curveBalancesBefore } = + await calcOethWithdrawAmount(fixture, withdrawAmount); + const oethSupplyBefore = await oSonic.totalSupply(); + const vaultWethBalanceBefore = await wS.balanceOf(oethVault.address); + + log("Before withdraw from strategy"); + await run("amoStrat", { + pool: "OS", + output: false, + }); + + // Now try to withdraw the WETH from the strategy + const tx = await swapXAMOStrategy + .connect(oSonicVaultSigner) + .withdraw(oethVault.address, wS.address, withdrawAmount); + + const receipt = await tx.wait(); + + log("After withdraw from strategy"); + await run("amoStrat", { + pool: "OS", + output: false, + fromBlock: receipt.blockNumber - 1, + }); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Withdrawal") + .withArgs(wS.address, swapXPool.address, withdrawAmount); + await expect(tx).to.emit(swapXAMOStrategy, "Withdrawal").withNamedArgs({ + _asset: oSonic.address, + _pToken: swapXPool.address, + }); + + // Check the ETH and OS balances in the Curve pool + const curveBalancesAfter = await swapXPool.get_balances(); + expect(curveBalancesAfter[0]).to.approxEqualTolerance( + curveBalancesBefore[0].sub(withdrawAmount), + 0.05 // 0.05% or 5 basis point + ); + expect(curveBalancesAfter[1]).to.approxEqualTolerance( + curveBalancesBefore[1].sub(oethBurnAmount), + 0.05 // 0.05% + ); + + // Check the OS total supply decrease + const oethSupplyAfter = await oSonic.totalSupply(); + expect(oethSupplyAfter).to.approxEqualTolerance( + oethSupplyBefore.sub(oethBurnAmount), + 0.05 // 0.05% or 5 basis point + ); + + // Check the WETH balance in the Vault + expect(await wS.balanceOf(oethVault.address)).to.equal( + vaultWethBalanceBefore.add(withdrawAmount) + ); + }); + it("Only vault can withdraw some WETH from AMO strategy", async function () { + const { swapXAMOStrategy, oethVault, strategist, timelock, nick, wS } = + fixture; + + for (const signer of [strategist, timelock, nick]) { + const tx = swapXAMOStrategy + .connect(signer) + .withdraw(oethVault.address, wS.address, parseUnits("50")); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + }); + it("Only vault and governor can withdraw all WETH from AMO strategy", async function () { + const { swapXAMOStrategy, strategist, timelock, nick } = fixture; + + for (const signer of [strategist, nick]) { + const tx = swapXAMOStrategy.connect(signer).withdrawAll(); + + await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); + } + + // Governor can withdraw all + const tx = swapXAMOStrategy.connect(timelock).withdrawAll(); + await expect(tx).to.emit(swapXAMOStrategy, "Withdrawal"); + }); + }); + + describe.skip("with a lot more OS in the pool", () => { + const loadFixture = createFixtureLoader(defaultSonicFixture, { + wsMintAmount: 5000, + depositToStrategy: false, + poolAddOethAmount: 60000, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Strategist should remove a little OS from the pool", async () => { + await assertRemoveAndBurn(parseUnits("3"), fixture); + }); + it("Strategist should remove a lot of OS from the pool", async () => { + const { cvxRewardPool, swapXAMOStrategy } = fixture; + const lpBalance = await cvxRewardPool.balanceOf(swapXAMOStrategy.address); + + const lpAmount = lpBalance + // reduce by 1% + .mul(99) + .div(100); + + await assertRemoveAndBurn(lpAmount, fixture); + }); + it("Strategist should fail to add even more OS to the pool", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + // Mint and add OS to the pool + const tx = swapXAMOStrategy + .connect(strategist) + .mintAndAddOTokens(parseUnits("1")); + + await expect(tx).to.be.revertedWith("OTokens balance worse"); + }); + }); + + describe.skip("with a lot more OS in the pool", () => { + const loadFixture = createFixtureLoader(defaultSonicFixture, { + wsMintAmount: 5000, + depositToStrategy: false, + poolAddOethAmount: 6000, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Strategist should fail to remove the little ETH from the pool", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + // Remove ETH form the pool + const tx = swapXAMOStrategy + .connect(strategist) + .removeOnlyAssets(parseUnits("1")); + + await expect(tx).to.be.revertedWith("OTokens balance worse"); + }); + }); + + describe.skip("with a lot more wS in the pool", () => { + const loadFixture = createFixtureLoader(defaultSonicFixture, { + wsMintAmount: 5000, + depositToStrategy: false, + poolAddEthAmount: 200000, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Strategist should add a little OS to the pool", async () => { + const oethMintAmount = oethUnits("3"); + await assertMintAndAddOTokens(oethMintAmount, fixture); + }); + it("Strategist should add a lot of OS to the pool", async () => { + const oethMintAmount = oethUnits("150000"); + await assertMintAndAddOTokens(oethMintAmount, fixture); + }); + it("Strategist should add OS to balance the pool", async () => { + const { swapXPool } = fixture; + const curveBalances = await swapXPool.get_balances(); + const oethMintAmount = curveBalances[0] + .sub(curveBalances[1]) + // reduce by 0.001% + .mul(99999) + .div(100000); + + await assertMintAndAddOTokens(oethMintAmount, fixture); + }); + it("Strategist should remove a little ETH from the pool", async () => { + const lpAmount = parseUnits("2"); + await assertRemoveOnlyAssets(lpAmount, fixture); + }); + it("Strategist should remove a lot ETH from the pool", async () => { + const { cvxRewardPool, swapXAMOStrategy } = fixture; + const lpBalance = await cvxRewardPool.balanceOf(swapXAMOStrategy.address); + const lpAmount = lpBalance + // reduce by 1% + .mul(99) + .div(100); + + await assertRemoveOnlyAssets(lpAmount, fixture); + }); + }); + + describe.skip("with a little more wS in the pool", () => { + const loadFixture = createFixtureLoader(defaultSonicFixture, { + wsMintAmount: 20000, + depositToStrategy: false, + poolAddEthAmount: 22000, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Strategist should remove ETH to balance the pool", async () => { + const { cvxRewardPool, swapXAMOStrategy } = fixture; + const lpBalance = await cvxRewardPool.balanceOf(swapXAMOStrategy.address); + const lpAmount = lpBalance + // reduce by 1% + .mul(99) + .div(100); + + await assertRemoveOnlyAssets(lpAmount, fixture); + }); + }); + + describe.skip("with a little more wS in the pool", () => { + const loadFixture = createFixtureLoader(defaultSonicFixture, { + wsMintAmount: 20000, + depositToStrategy: true, + poolAddEthAmount: 8000, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Strategist should fail to add too much OS to the pool", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + // Add OS to the pool + const tx = swapXAMOStrategy + .connect(strategist) + .mintAndAddOTokens(parseUnits("10000")); + + await expect(tx).to.be.revertedWith("Assets overshot peg"); + }); + it("Strategist should fail to remove too much ETH from the pool", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + // Remove ETH from the pool + const tx = swapXAMOStrategy + .connect(strategist) + .removeOnlyAssets(parseUnits("8000")); + + await expect(tx).to.be.revertedWith("Assets overshot peg"); + }); + it("Strategist should fail to remove the little OS from the pool", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + // Remove ETH from the pool + const tx = swapXAMOStrategy + .connect(strategist) + .removeAndBurnOTokens(parseUnits("1")); + + await expect(tx).to.be.revertedWith("Assets balance worse"); + }); + }); + + describe.skip("with a little more OS in the pool", () => { + const loadFixture = createFixtureLoader(defaultSonicFixture, { + wsMintAmount: 20000, + depositToStrategy: false, + poolAddOethAmount: 5000, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("Strategist should fail to remove too much OS from the pool", async () => { + const { cvxRewardPool, swapXAMOStrategy, strategist } = fixture; + const lpBalance = await cvxRewardPool.balanceOf(swapXAMOStrategy.address); + const lpAmount = lpBalance + // reduce by 1% + .mul(99) + .div(100); + + // Remove OS from the pool + const tx = swapXAMOStrategy + .connect(strategist) + .removeAndBurnOTokens(lpAmount); + + await expect(tx).to.be.revertedWith("OTokens overshot peg"); + }); + }); +}); + +async function assertRemoveAndBurn(lpAmount, fixture) { + const { swapXAMOStrategy, swapXPool, oSonic, strategist } = fixture; + + const oethBurnAmount = await calcOethRemoveAmount(fixture, lpAmount); + const curveBalancesBefore = await swapXPool.get_balances(); + const oethSupplyBefore = await oSonic.totalSupply(); + + log(`Before remove and burn of ${formatUnits(lpAmount)} OS from the pool`); + await run("amoStrat", { + pool: "OS", + output: false, + }); + + // Remove and burn OS from the pool + const tx = await swapXAMOStrategy + .connect(strategist) + .removeAndBurnOTokens(lpAmount); + + const receipt = await tx.wait(); + + log("After remove and burn of OS from pool"); + await run("amoStrat", { + pool: "OS", + output: false, + fromBlock: receipt.blockNumber - 1, + }); + + // Check emitted event + await expect(tx) + .to.emit(swapXAMOStrategy, "Withdrawal") + .withArgs(oSonic.address, swapXPool.address, oethBurnAmount); + + // Check the ETH and OS balances in the Curve pool + const curveBalancesAfter = await swapXPool.get_balances(); + expect(curveBalancesAfter[0]).to.equal(curveBalancesBefore[0]); + expect(curveBalancesAfter[1]).to.approxEqualTolerance( + curveBalancesBefore[1].sub(oethBurnAmount), + 0.01 // 0.01% + ); + + // Check the OS total supply decrease + const oethSupplyAfter = await oSonic.totalSupply(); + expect(oethSupplyAfter).to.approxEqualTolerance( + oethSupplyBefore.sub(oethBurnAmount), + 0.01 // 0.01% or 1 basis point + ); +} + +async function assertMintAndAddOTokens(oethMintAmount, fixture) { + const { swapXAMOStrategy, swapXPool, oSonic, strategist } = fixture; + + const curveBalancesBefore = await swapXPool.get_balances(); + const oethSupplyBefore = await oSonic.totalSupply(); + + log(`Before mint and add ${formatUnits(oethMintAmount)} OS to the pool`); + await run("amoStrat", { + pool: "OS", + output: false, + }); + + // Mint and add OS to the pool + const tx = await swapXAMOStrategy + .connect(strategist) + .mintAndAddOTokens(oethMintAmount); + + const receipt = await tx.wait(); + + // Check emitted event + await expect(tx) + .emit(swapXAMOStrategy, "Deposit") + .withArgs(oSonic.address, swapXPool.address, oethMintAmount); + + log("After mint and add of OS to the pool"); + await run("amoStrat", { + pool: "OS", + output: false, + fromBlock: receipt.blockNumber - 1, + }); + + // Check the ETH and OS balances in the Curve pool + const curveBalancesAfter = await swapXPool.get_balances(); + expect(curveBalancesAfter[0]).to.approxEqualTolerance( + curveBalancesBefore[0], + 0.01 // 0.01% or 1 basis point + ); + expect(curveBalancesAfter[1]).to.approxEqualTolerance( + curveBalancesBefore[1].add(oethMintAmount), + 0.01 // 0.01% + ); + + // Check the OS total supply decrease + const oethSupplyAfter = await oSonic.totalSupply(); + expect(oethSupplyAfter).to.approxEqualTolerance( + oethSupplyBefore.add(oethMintAmount), + 0.01 // 0.01% or 1 basis point + ); +} + +async function assertRemoveOnlyAssets(lpAmount, fixture) { + const { + swapXAMOStrategy, + cvxRewardPool, + swapXPool, + oethVault, + oSonic, + strategist, + wS, + } = fixture; + + log(`Removing ${formatUnits(lpAmount)} ETH from the pool`); + const ethRemoveAmount = await calcEthRemoveAmount(fixture, lpAmount); + log("After calc ETH remove amount"); + const curveBalancesBefore = await swapXPool.get_balances(); + const oethSupplyBefore = await oSonic.totalSupply(); + const vaultWethBalanceBefore = await wS.balanceOf(oethVault.address); + const strategyLpBalanceBefore = await cvxRewardPool.balanceOf( + swapXAMOStrategy.address + ); + const vaultValueBefore = await oethVault.totalValue(); + + log(`Before remove and burn of ${formatUnits(lpAmount)} ETH from the pool`); + await run("amoStrat", { + pool: "OS", + output: false, + }); + + // Remove ETH from the pool and transfer to the Vault as WETH + const tx = await swapXAMOStrategy + .connect(strategist) + .removeOnlyAssets(lpAmount); + + const receipt = await tx.wait(); + + log("After remove and burn of ETH from pool"); + await run("amoStrat", { + pool: "OS", + output: false, + fromBlock: receipt.blockNumber - 1, + }); + + // Check emitted event + await expect(tx) + .to.emit(swapXAMOStrategy, "Withdrawal") + .withArgs(wS.address, swapXPool.address, ethRemoveAmount); + + // Check the ETH and OS balances in the Curve pool + const curveBalancesAfter = await swapXPool.get_balances(); + expect(curveBalancesAfter[0]).to.approxEqualTolerance( + curveBalancesBefore[0].sub(ethRemoveAmount), + 0.01 // 0.01% or 1 basis point + ); + expect(curveBalancesAfter[1]).to.equal(curveBalancesBefore[1]); + + // Check the OS total supply is the same + const oethSupplyAfter = await oSonic.totalSupply(); + expect(oethSupplyAfter).to.approxEqualTolerance( + oethSupplyBefore, + 0.01 // 0.01% or 1 basis point + ); + + // Check the WETH balance in the Vault + expect(await wS.balanceOf(oethVault.address)).to.equal( + vaultWethBalanceBefore.add(ethRemoveAmount) + ); + + // Check the vault made money + const vaultValueAfter = await oethVault.totalValue(); + expect(vaultValueAfter.sub(vaultValueBefore)).to.gt(parseUnits("-1")); + + // Check the strategy LP balance decreased + const strategyLpBalanceAfter = await cvxRewardPool.balanceOf( + swapXAMOStrategy.address + ); + expect(strategyLpBalanceBefore.sub(strategyLpBalanceAfter)).to.eq(lpAmount); +} + +// Calculate the minted OS amount for a deposit +async function calcOSMintAmount(fixture, wsDepositAmount) { + const { swapXPool } = fixture; + + // Get the wS and OS balances in the pool + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + console.log("wsBalance", wsReserves); + console.log("osBalance", osReserves); + // wS balance - OS balance + const balanceDiff = wsReserves.sub(osReserves); + + let osMintAmount = balanceDiff.lte(0) + ? // If more OS than wS then mint same amount of OS as wS + wsDepositAmount + : // If less OS than wS then mint the difference + balanceDiff.add(wsDepositAmount); + // Cap the minting to twice the ETH deposit amount + const doubleWethDepositAmount = wsDepositAmount.mul(2); + osMintAmount = osMintAmount.lte(doubleWethDepositAmount) + ? osMintAmount + : doubleWethDepositAmount; + log(`OS mint amount : ${formatUnits(osMintAmount)}`); + + return { osMintAmount, wsReserves, osReserves }; +} + +// Calculate the amount of OS burnt from a withdraw +async function calcOethWithdrawAmount(fixture, wethWithdrawAmount) { + const { swapXPool } = fixture; + + // Get the ETH and OS balances in the Curve pool + const curveBalances = await swapXPool.get_balances(); + + // OS to burn = WETH withdrawn * OS pool balance / ETH pool balance + const oethBurnAmount = wethWithdrawAmount + .mul(curveBalances[1]) + .div(curveBalances[0]); + + log(`OS burn amount : ${formatUnits(oethBurnAmount)}`); + + return { oethBurnAmount, curveBalances }; +} + +// Calculate the OS and ETH amounts from a withdrawAll +async function calcWithdrawAllAmounts(fixture) { + const { swapXAMOStrategy, cvxRewardPool, swapXPool } = fixture; + + // Get the ETH and OS balances in the Curve pool + const curveBalances = await swapXPool.get_balances(); + const strategyLpAmount = await cvxRewardPool.balanceOf( + swapXAMOStrategy.address + ); + const totalLpSupply = await swapXPool.totalSupply(); + + // OS to burn = OS pool balance * strategy LP amount / total pool LP amount + const oethBurnAmount = curveBalances[1] + .mul(strategyLpAmount) + .div(totalLpSupply); + // ETH to withdraw = ETH pool balance * strategy LP amount / total pool LP amount + const ethWithdrawAmount = curveBalances[0] + .mul(strategyLpAmount) + .div(totalLpSupply); + + log(`OS burn amount : ${formatUnits(oethBurnAmount)}`); + log(`ETH withdraw amount : ${formatUnits(ethWithdrawAmount)}`); + + return { oethBurnAmount, ethWithdrawAmount, curveBalances }; +} + +// Calculate the amount of OS burned from a removeAndBurnOTokens +async function calcOethRemoveAmount(fixture, lpAmount) { + const { oethGaugeSigner, swapXPool } = fixture; + + // Static call to get the OS removed from the pool for a given amount of LP tokens + const oethBurnAmount = await swapXPool + .connect(oethGaugeSigner) + .callStatic["remove_liquidity_one_coin(uint256,int128,uint256)"]( + lpAmount, + 1, + 0 + ); + + log(`OS burn amount : ${formatUnits(oethBurnAmount)}`); + + return oethBurnAmount; +} + +// Calculate the amount of ETH burned from a removeOnlyAssets +async function calcEthRemoveAmount(fixture, lpAmount) { + const { swapXPool } = fixture; + + // Get the ETH removed from the pool for a given amount of LP tokens + const ethRemoveAmount = await swapXPool.calc_withdraw_one_coin(lpAmount, 0); + + log(`ETH burn amount : ${formatUnits(ethRemoveAmount)}`); + + return ethRemoveAmount; +} diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 8f4d23c368..9fe490ab8b 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -405,9 +405,9 @@ addresses.sonic.SwapXOsGEMSx.pool = "0x9ac7F5961a452e9cD5Be5717bD2c3dF412D1c1a5"; addresses.sonic.SwapXWSOS = {}; -addresses.sonic.SwapXWSOS.pool = "0xcfe67b6c7b65c8d038e666b3241a161888b7f2b0"; +addresses.sonic.SwapXWSOS.pool = "0xcfE67b6c7B65c8d038e666b3241a161888B7f2b0"; // TODO replace once the gauge is deployed -addresses.sonic.SwapXWSOS.gauge = "0x2e5228cfecfefa439f92aa4823e5593de5bfcb7d"; +addresses.sonic.SwapXWSOS.gauge = "0x2E5228cFEcFEFA439f92Aa4823E5593de5bFcB7D"; addresses.sonic.SwapXOsUSDCeMultisigBooster = "0x4636269e7CDc253F6B0B210215C3601558FE80F6"; From a951e107ac1ae9ef197b4110681a31ab6ef53a93 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 26 Feb 2025 11:45:51 +1100 Subject: [PATCH 37/80] Update SwapX AMO fork tests --- contracts/test/_fixture-sonic.js | 56 ++++----- .../sonic/swapx-amo.sonic.fork-test.js | 108 ++++++++---------- 2 files changed, 73 insertions(+), 91 deletions(-) diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 5a90267819..e3af57b47f 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -157,10 +157,7 @@ const defaultSonicFixture = deployments.createFixture(async () => { curvePool, curveGauge, curveChildLiquidityGaugeFactory, - crv, - swapXAMOStrategy, - swapXPool, - swapXGauge; + crv; if (isFork) { validatorRegistrator = await impersonateAndFund( addresses.sonic.validatorRegistrator @@ -194,25 +191,6 @@ const defaultSonicFixture = deployments.createFixture(async () => { ); crv = await ethers.getContractAt(erc20Abi, addresses.sonic.CRV); - - // SwapX AMO - const swapXAMOProxy = await ethers.getContract( - "SonicSwapXAMOStrategyProxy" - ); - swapXAMOStrategy = await ethers.getContractAt( - "SonicSwapXAMOStrategy", - swapXAMOProxy.address - ); - - swapXPool = await ethers.getContractAt( - "IPair", - addresses.sonic.SwapXWSOS.pool - ); - - swapXGauge = await ethers.getContractAt( - "IGauge", - addresses.sonic.SwapXWSOS.gauge - ); } for (const user of [rafael, nick, clement]) { @@ -247,11 +225,6 @@ const defaultSonicFixture = deployments.createFixture(async () => { curveChildLiquidityGaugeFactory, crv, - // SwapX - swapXAMOStrategy, - swapXPool, - swapXGauge, - // Signers governor, strategist, @@ -284,8 +257,29 @@ async function swapXAMOFixture( ) { const fixture = await defaultSonicFixture(); - const { oSonicVault, nick, strategist, swapXAMOStrategy, timelock, wS } = - fixture; + const { oSonicVault, nick, strategist, timelock, wS } = fixture; + + let swapXAMOStrategy, swapXPool, swapXGauge; + + if (isFork) { + const swapXAMOProxy = await ethers.getContract( + "SonicSwapXAMOStrategyProxy" + ); + swapXAMOStrategy = await ethers.getContractAt( + "SonicSwapXAMOStrategy", + swapXAMOProxy.address + ); + + swapXPool = await ethers.getContractAt( + "IPair", + addresses.sonic.SwapXWSOS.pool + ); + + swapXGauge = await ethers.getContractAt( + "IGauge", + addresses.sonic.SwapXWSOS.gauge + ); + } await oSonicVault .connect(timelock) @@ -322,7 +316,7 @@ async function swapXAMOFixture( } } - return fixture; + return { ...fixture, swapXAMOStrategy, swapXPool, swapXGauge }; } const deployPoolBoosterFactorySwapxSingle = async ( diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index bd834a8d08..5d32cb39c0 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -3,10 +3,7 @@ const { formatUnits, parseUnits } = require("ethers/lib/utils"); const { run } = require("hardhat"); const { createFixtureLoader } = require("../../_fixture"); -const { - defaultSonicFixture, - swapXAMOFixture, -} = require("../../_fixture-sonic"); +const { swapXAMOFixture } = require("../../_fixture-sonic"); const { units, oethUnits, isCI } = require("../../helpers"); const addresses = require("../../../utils/addresses"); @@ -19,7 +16,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { let fixture; describe("post deployment", () => { - const loadFixture = createFixtureLoader(defaultSonicFixture); + const loadFixture = createFixtureLoader(swapXAMOFixture); beforeEach(async () => { fixture = await loadFixture(); }); @@ -86,7 +83,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - describe.skip("with some wS in the vault", () => { + describe("with some wS in the vault", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: false, @@ -96,39 +93,39 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Vault should deposit some wS to AMO strategy", async function () { - const { swapXAMOStrategy, oSonic, swapXPool, oSonicVaultSigner, wS } = - fixture; + const { + swapXAMOStrategy, + oSonic, + swapXPool, + swapXGauge, + oSonicVaultSigner, + wS, + } = fixture; const wsDepositAmount = await units("5000", wS); - // Vault transfers WETH to strategy + // Vault transfers wS to strategy await wS .connect(oSonicVaultSigner) .transfer(swapXAMOStrategy.address, wsDepositAmount); - const { osMintAmount, curveBalances: curveBalancesBefore } = - await calcOSMintAmount(fixture, wsDepositAmount); + const { + osMintAmount, + wsReserves: wsReservesBefore, + osReserves: osReservesBefore, + } = await calcOSMintAmount(fixture, wsDepositAmount); const osSupplyBefore = await oSonic.totalSupply(); - - // log("Before deposit to strategy"); - // await run("amoStrat", { - // pool: "OS", - // output: false, - // }); + const stratBalanceBefore = await swapXAMOStrategy.checkBalance( + wS.address + ); + const stratGaugeBalanceBefore = await swapXGauge.balanceOf( + swapXAMOStrategy.address + ); const tx = await swapXAMOStrategy .connect(oSonicVaultSigner) .deposit(wS.address, wsDepositAmount); - // const receipt = await tx.wait(); - - // log("After deposit to strategy"); - // await run("amoStrat", { - // pool: "OS", - // output: false, - // fromBlock: receipt.blockNumber - 1, - // }); - // Check emitted events await expect(tx) .to.emit(swapXAMOStrategy, "Deposit") @@ -137,22 +134,26 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .to.emit(swapXAMOStrategy, "Deposit") .withArgs(oSonic.address, swapXPool.address, osMintAmount); - // Check the ETH and OS balances in the Curve pool - const curveBalancesAfter = await swapXPool.get_balances(); - expect(curveBalancesAfter[0]).to.approxEqualTolerance( - curveBalancesBefore[0].add(wsDepositAmount), - 0.01 // 0.01% or 1 basis point - ); - expect(curveBalancesAfter[1]).to.approxEqualTolerance( - curveBalancesBefore[1].add(osMintAmount), - 0.01 // 0.01% or 1 basis point + // Check the strategy balance increased + expect( + await swapXAMOStrategy.checkBalance(wS.address) + ).to.approxEqualTolerance( + stratBalanceBefore.add(wsDepositAmount).add(osMintAmount) ); // Check the OS total supply increase const oethSupplyAfter = await oSonic.totalSupply(); - expect(oethSupplyAfter).to.approxEqualTolerance( - osSupplyBefore.add(osMintAmount), - 0.01 // 0.01% or 1 basis point + expect(oethSupplyAfter).to.equal(osSupplyBefore.add(osMintAmount)); + + // Check the reserves in the pool + const { _reserve0: wsReservesAfter, _reserve1: osReservesAfter } = + await swapXPool.getReserves(); + expect(wsReservesAfter).to.equal(wsReservesBefore.add(wsDepositAmount)); + expect(osReservesAfter).to.equal(osReservesBefore.add(osMintAmount)); + + // Check the strategy's gauge balance increased + expect(await swapXGauge.balanceOf(swapXAMOStrategy.address)).to.gt( + stratGaugeBalanceBefore ); }); it("Only vault can deposit some wS to AMO strategy", async function () { @@ -208,7 +209,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); describe.skip("with the strategy having some OS and wS in the pool", () => { - const loadFixture = createFixtureLoader(defaultSonicFixture, { + const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: true, balancePool: true, @@ -371,7 +372,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); describe.skip("with a lot more OS in the pool", () => { - const loadFixture = createFixtureLoader(defaultSonicFixture, { + const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: false, poolAddOethAmount: 60000, @@ -407,7 +408,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); describe.skip("with a lot more OS in the pool", () => { - const loadFixture = createFixtureLoader(defaultSonicFixture, { + const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: false, poolAddOethAmount: 6000, @@ -429,7 +430,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); describe.skip("with a lot more wS in the pool", () => { - const loadFixture = createFixtureLoader(defaultSonicFixture, { + const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: false, poolAddEthAmount: 200000, @@ -474,7 +475,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); describe.skip("with a little more wS in the pool", () => { - const loadFixture = createFixtureLoader(defaultSonicFixture, { + const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 20000, depositToStrategy: false, poolAddEthAmount: 22000, @@ -496,7 +497,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); describe.skip("with a little more wS in the pool", () => { - const loadFixture = createFixtureLoader(defaultSonicFixture, { + const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 20000, depositToStrategy: true, poolAddEthAmount: 8000, @@ -538,7 +539,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); describe.skip("with a little more OS in the pool", () => { - const loadFixture = createFixtureLoader(defaultSonicFixture, { + const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 20000, depositToStrategy: false, poolAddOethAmount: 5000, @@ -748,21 +749,8 @@ async function calcOSMintAmount(fixture, wsDepositAmount) { // Get the wS and OS balances in the pool const { _reserve0: wsReserves, _reserve1: osReserves } = await swapXPool.getReserves(); - console.log("wsBalance", wsReserves); - console.log("osBalance", osReserves); - // wS balance - OS balance - const balanceDiff = wsReserves.sub(osReserves); - - let osMintAmount = balanceDiff.lte(0) - ? // If more OS than wS then mint same amount of OS as wS - wsDepositAmount - : // If less OS than wS then mint the difference - balanceDiff.add(wsDepositAmount); - // Cap the minting to twice the ETH deposit amount - const doubleWethDepositAmount = wsDepositAmount.mul(2); - osMintAmount = osMintAmount.lte(doubleWethDepositAmount) - ? osMintAmount - : doubleWethDepositAmount; + + const osMintAmount = wsDepositAmount.mul(osReserves).div(wsReserves); log(`OS mint amount : ${formatUnits(osMintAmount)}`); return { osMintAmount, wsReserves, osReserves }; From ec174e90a246d28de9b7b9b176d0b9d4112c5105 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 26 Feb 2025 13:57:28 +1100 Subject: [PATCH 38/80] More SwapX AMO fork tests --- .../sonic/swapx-amo.sonic.fork-test.js | 300 ++++++++---------- 1 file changed, 139 insertions(+), 161 deletions(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 5d32cb39c0..e2fdab04cf 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -104,24 +104,23 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const wsDepositAmount = await units("5000", wS); - // Vault transfers wS to strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, wsDepositAmount); - - const { - osMintAmount, - wsReserves: wsReservesBefore, - osReserves: osReservesBefore, - } = await calcOSMintAmount(fixture, wsDepositAmount); - const osSupplyBefore = await oSonic.totalSupply(); const stratBalanceBefore = await swapXAMOStrategy.checkBalance( wS.address ); + const osSupplyBefore = await oSonic.totalSupply(); + const { osMintAmount, reserves: reservesBefore } = await calcOSMintAmount( + fixture, + wsDepositAmount + ); const stratGaugeBalanceBefore = await swapXGauge.balanceOf( swapXAMOStrategy.address ); + // Vault transfers wS to strategy + await wS + .connect(oSonicVaultSigner) + .transfer(swapXAMOStrategy.address, wsDepositAmount); + // Vault calls deposit on the strategy const tx = await swapXAMOStrategy .connect(oSonicVaultSigner) .deposit(wS.address, wsDepositAmount); @@ -135,21 +134,24 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .withArgs(oSonic.address, swapXPool.address, osMintAmount); // Check the strategy balance increased - expect( - await swapXAMOStrategy.checkBalance(wS.address) - ).to.approxEqualTolerance( - stratBalanceBefore.add(wsDepositAmount).add(osMintAmount) + const expectedStratBalance = stratBalanceBefore + .add(wsDepositAmount) + .add(osMintAmount); + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( + expectedStratBalance.sub(1), + expectedStratBalance ); // Check the OS total supply increase - const oethSupplyAfter = await oSonic.totalSupply(); - expect(oethSupplyAfter).to.equal(osSupplyBefore.add(osMintAmount)); + expect(await oSonic.totalSupply()).to.equal( + osSupplyBefore.add(osMintAmount) + ); // Check the reserves in the pool const { _reserve0: wsReservesAfter, _reserve1: osReservesAfter } = await swapXPool.getReserves(); - expect(wsReservesAfter).to.equal(wsReservesBefore.add(wsDepositAmount)); - expect(osReservesAfter).to.equal(osReservesBefore.add(osMintAmount)); + expect(wsReservesAfter).to.equal(reservesBefore.ws.add(wsDepositAmount)); + expect(osReservesAfter).to.equal(reservesBefore.os.add(osMintAmount)); // Check the strategy's gauge balance increased expect(await swapXGauge.balanceOf(swapXAMOStrategy.address)).to.gt( @@ -208,7 +210,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - describe.skip("with the strategy having some OS and wS in the pool", () => { + describe("with the strategy having some OS and wS in the pool", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: true, @@ -221,58 +223,48 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const { swapXAMOStrategy, swapXPool, oSonic, oSonicVaultSigner, wS } = fixture; + const stratBalanceBefore = await swapXAMOStrategy.checkBalance( + wS.address + ); const { - oethBurnAmount, - ethWithdrawAmount, - curveBalances: curveBalancesBefore, + osBurnAmount, + wsWithdrawAmount, + reserves: reservesBefore, } = await calcWithdrawAllAmounts(fixture); - const oethSupplyBefore = await oSonic.totalSupply(); - - log("Before withdraw all from strategy"); - await run("amoStrat", { - pool: "OS", - output: false, - }); + const osSupplyBefore = await oSonic.totalSupply(); - // Now try to withdraw all the WETH from the strategy + // Now try to withdraw all the wS from the strategy const tx = await swapXAMOStrategy .connect(oSonicVaultSigner) .withdrawAll(); - const receipt = await tx.wait(); - - log("After withdraw all from strategy"); - await run("amoStrat", { - pool: "OS", - output: false, - fromBlock: receipt.blockNumber - 1, - }); - // Check emitted events await expect(tx) .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(wS.address, swapXPool.address, ethWithdrawAmount); + .withArgs(wS.address, swapXPool.address, wsWithdrawAmount); await expect(tx) .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(oSonic.address, swapXPool.address, oethBurnAmount); - - // Check the ETH and OS balances in the Curve pool - const curveBalancesAfter = await swapXPool.get_balances(); - expect(curveBalancesAfter[0]).to.approxEqualTolerance( - curveBalancesBefore[0].sub(ethWithdrawAmount), - 0.05 // 0.05% or 5 basis point - ); - expect(curveBalancesAfter[1]).to.approxEqualTolerance( - curveBalancesBefore[1].sub(oethBurnAmount), - 0.05 // 0.05% + .withArgs(oSonic.address, swapXPool.address, osBurnAmount); + + // Check the strategy balance decreased + const expectedStratBalance = stratBalanceBefore + .sub(wsWithdrawAmount) + .sub(osBurnAmount); + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( + expectedStratBalance.sub(1), + expectedStratBalance ); + // Check the wS and OS reserves in the pool + const { _reserve0: wsReservesAfter, _reserve1: osReservesAfter } = + await swapXPool.getReserves(); + expect(wsReservesAfter).to.equal(reservesBefore.ws.sub(wsWithdrawAmount)); + expect(osReservesAfter).to.equal(reservesBefore.os.sub(osBurnAmount)); + // Check the OS total supply decrease - const oethSupplyAfter = await oSonic.totalSupply(); - expect(oethSupplyAfter).to.approxEqualTolerance( - oethSupplyBefore.sub(oethBurnAmount), - 0.05 // 0.01% or 5 basis point + expect(await oSonic.totalSupply()).to.equal( + osSupplyBefore.sub(osBurnAmount) ); }); it("Vault should be able to withdraw some", async () => { @@ -280,78 +272,68 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { swapXAMOStrategy, oSonic, swapXPool, - oethVault, + oSonicVault, oSonicVaultSigner, wS, } = fixture; - const withdrawAmount = oethUnits("1000"); - - const { oethBurnAmount, curveBalances: curveBalancesBefore } = - await calcOethWithdrawAmount(fixture, withdrawAmount); - const oethSupplyBefore = await oSonic.totalSupply(); - const vaultWethBalanceBefore = await wS.balanceOf(oethVault.address); + const wsWithdrawAmount = oethUnits("1000"); - log("Before withdraw from strategy"); - await run("amoStrat", { - pool: "OS", - output: false, - }); + const stratBalanceBefore = await swapXAMOStrategy.checkBalance( + wS.address + ); + const { osBurnAmount, reserves: reservesBefore } = + await calcOSWithdrawAmount(fixture, wsWithdrawAmount); + const osSupplyBefore = await oSonic.totalSupply(); + const vaultWSBalanceBefore = await wS.balanceOf(oSonicVault.address); - // Now try to withdraw the WETH from the strategy + // Now try to withdraw the wS from the strategy const tx = await swapXAMOStrategy .connect(oSonicVaultSigner) - .withdraw(oethVault.address, wS.address, withdrawAmount); - - const receipt = await tx.wait(); - - log("After withdraw from strategy"); - await run("amoStrat", { - pool: "OS", - output: false, - fromBlock: receipt.blockNumber - 1, - }); + .withdraw(oSonicVault.address, wS.address, wsWithdrawAmount); // Check emitted events await expect(tx) .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(wS.address, swapXPool.address, withdrawAmount); + .withArgs(wS.address, swapXPool.address, wsWithdrawAmount); await expect(tx).to.emit(swapXAMOStrategy, "Withdrawal").withNamedArgs({ _asset: oSonic.address, _pToken: swapXPool.address, }); - // Check the ETH and OS balances in the Curve pool - const curveBalancesAfter = await swapXPool.get_balances(); - expect(curveBalancesAfter[0]).to.approxEqualTolerance( - curveBalancesBefore[0].sub(withdrawAmount), - 0.05 // 0.05% or 5 basis point - ); - expect(curveBalancesAfter[1]).to.approxEqualTolerance( - curveBalancesBefore[1].sub(oethBurnAmount), - 0.05 // 0.05% + // Check the strategy balance decreased + const expectedStratBalance = stratBalanceBefore + .sub(wsWithdrawAmount) + .sub(osBurnAmount); + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( + expectedStratBalance.sub(1), + expectedStratBalance ); + // Check the wS and OS reserves in the pool + const { _reserve0: wsReservesAfter, _reserve1: osReservesAfter } = + await swapXPool.getReserves(); + expect(wsReservesAfter).to.equal(reservesBefore.ws.sub(wsWithdrawAmount)); + expect(osReservesAfter).to.equal(reservesBefore.os.sub(osBurnAmount)); + // Check the OS total supply decrease - const oethSupplyAfter = await oSonic.totalSupply(); - expect(oethSupplyAfter).to.approxEqualTolerance( - oethSupplyBefore.sub(oethBurnAmount), - 0.05 // 0.05% or 5 basis point + expect(await oSonic.totalSupply()).to.equal( + osSupplyBefore.sub(osBurnAmount) ); - // Check the WETH balance in the Vault - expect(await wS.balanceOf(oethVault.address)).to.equal( - vaultWethBalanceBefore.add(withdrawAmount) + // Check the wS balance in the Vault + expect(await wS.balanceOf(oSonicVault.address)).to.equal( + vaultWSBalanceBefore.add(wsWithdrawAmount) ); }); it("Only vault can withdraw some WETH from AMO strategy", async function () { - const { swapXAMOStrategy, oethVault, strategist, timelock, nick, wS } = + const { swapXAMOStrategy, oSonicVault, strategist, timelock, nick, wS } = fixture; for (const signer of [strategist, timelock, nick]) { const tx = swapXAMOStrategy .connect(signer) - .withdraw(oethVault.address, wS.address, parseUnits("50")); + .withdraw(oSonicVault.address, wS.address, parseUnits("50")); await expect(tx).to.revertedWith("Caller is not the Vault"); } @@ -385,8 +367,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await assertRemoveAndBurn(parseUnits("3"), fixture); }); it("Strategist should remove a lot of OS from the pool", async () => { - const { cvxRewardPool, swapXAMOStrategy } = fixture; - const lpBalance = await cvxRewardPool.balanceOf(swapXAMOStrategy.address); + const { swapXGauge, swapXAMOStrategy } = fixture; + const lpBalance = await swapXGauge.balanceOf(swapXAMOStrategy.address); const lpAmount = lpBalance // reduce by 1% @@ -463,8 +445,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await assertRemoveOnlyAssets(lpAmount, fixture); }); it("Strategist should remove a lot ETH from the pool", async () => { - const { cvxRewardPool, swapXAMOStrategy } = fixture; - const lpBalance = await cvxRewardPool.balanceOf(swapXAMOStrategy.address); + const { swapXGauge, swapXAMOStrategy } = fixture; + const lpBalance = await swapXGauge.balanceOf(swapXAMOStrategy.address); const lpAmount = lpBalance // reduce by 1% .mul(99) @@ -485,8 +467,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Strategist should remove ETH to balance the pool", async () => { - const { cvxRewardPool, swapXAMOStrategy } = fixture; - const lpBalance = await cvxRewardPool.balanceOf(swapXAMOStrategy.address); + const { swapXGauge, swapXAMOStrategy } = fixture; + const lpBalance = await swapXGauge.balanceOf(swapXAMOStrategy.address); const lpAmount = lpBalance // reduce by 1% .mul(99) @@ -549,8 +531,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Strategist should fail to remove too much OS from the pool", async () => { - const { cvxRewardPool, swapXAMOStrategy, strategist } = fixture; - const lpBalance = await cvxRewardPool.balanceOf(swapXAMOStrategy.address); + const { swapXGauge, swapXAMOStrategy, strategist } = fixture; + const lpBalance = await swapXGauge.balanceOf(swapXAMOStrategy.address); const lpAmount = lpBalance // reduce by 1% .mul(99) @@ -569,9 +551,9 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { async function assertRemoveAndBurn(lpAmount, fixture) { const { swapXAMOStrategy, swapXPool, oSonic, strategist } = fixture; - const oethBurnAmount = await calcOethRemoveAmount(fixture, lpAmount); + const oethBurnAmount = await calcOSRemoveAmount(fixture, lpAmount); const curveBalancesBefore = await swapXPool.get_balances(); - const oethSupplyBefore = await oSonic.totalSupply(); + const osSupplyBefore = await oSonic.totalSupply(); log(`Before remove and burn of ${formatUnits(lpAmount)} OS from the pool`); await run("amoStrat", { @@ -607,9 +589,8 @@ async function assertRemoveAndBurn(lpAmount, fixture) { ); // Check the OS total supply decrease - const oethSupplyAfter = await oSonic.totalSupply(); - expect(oethSupplyAfter).to.approxEqualTolerance( - oethSupplyBefore.sub(oethBurnAmount), + expect(await oSonic.totalSupply()).to.approxEqualTolerance( + osSupplyBefore.sub(oethBurnAmount), 0.01 // 0.01% or 1 basis point ); } @@ -618,7 +599,7 @@ async function assertMintAndAddOTokens(oethMintAmount, fixture) { const { swapXAMOStrategy, swapXPool, oSonic, strategist } = fixture; const curveBalancesBefore = await swapXPool.get_balances(); - const oethSupplyBefore = await oSonic.totalSupply(); + const osSupplyBefore = await oSonic.totalSupply(); log(`Before mint and add ${formatUnits(oethMintAmount)} OS to the pool`); await run("amoStrat", { @@ -657,9 +638,8 @@ async function assertMintAndAddOTokens(oethMintAmount, fixture) { ); // Check the OS total supply decrease - const oethSupplyAfter = await oSonic.totalSupply(); - expect(oethSupplyAfter).to.approxEqualTolerance( - oethSupplyBefore.add(oethMintAmount), + expect(await oSonic.totalSupply()).to.approxEqualTolerance( + osSupplyBefore.add(oethMintAmount), 0.01 // 0.01% or 1 basis point ); } @@ -667,24 +647,24 @@ async function assertMintAndAddOTokens(oethMintAmount, fixture) { async function assertRemoveOnlyAssets(lpAmount, fixture) { const { swapXAMOStrategy, - cvxRewardPool, + swapXGauge, swapXPool, - oethVault, + oSonicVault, oSonic, strategist, wS, } = fixture; log(`Removing ${formatUnits(lpAmount)} ETH from the pool`); - const ethRemoveAmount = await calcEthRemoveAmount(fixture, lpAmount); + const ethRemoveAmount = await calcWSRemoveAmount(fixture, lpAmount); log("After calc ETH remove amount"); const curveBalancesBefore = await swapXPool.get_balances(); - const oethSupplyBefore = await oSonic.totalSupply(); - const vaultWethBalanceBefore = await wS.balanceOf(oethVault.address); - const strategyLpBalanceBefore = await cvxRewardPool.balanceOf( + const osSupplyBefore = await oSonic.totalSupply(); + const vaultWethBalanceBefore = await wS.balanceOf(oSonicVault.address); + const strategyLpBalanceBefore = await swapXGauge.balanceOf( swapXAMOStrategy.address ); - const vaultValueBefore = await oethVault.totalValue(); + const vaultValueBefore = await oSonicVault.totalValue(); log(`Before remove and burn of ${formatUnits(lpAmount)} ETH from the pool`); await run("amoStrat", { @@ -720,23 +700,22 @@ async function assertRemoveOnlyAssets(lpAmount, fixture) { expect(curveBalancesAfter[1]).to.equal(curveBalancesBefore[1]); // Check the OS total supply is the same - const oethSupplyAfter = await oSonic.totalSupply(); - expect(oethSupplyAfter).to.approxEqualTolerance( - oethSupplyBefore, + expect(await oSonic.totalSupply()).to.approxEqualTolerance( + osSupplyBefore, 0.01 // 0.01% or 1 basis point ); // Check the WETH balance in the Vault - expect(await wS.balanceOf(oethVault.address)).to.equal( + expect(await wS.balanceOf(oSonicVault.address)).to.equal( vaultWethBalanceBefore.add(ethRemoveAmount) ); // Check the vault made money - const vaultValueAfter = await oethVault.totalValue(); + const vaultValueAfter = await oSonicVault.totalValue(); expect(vaultValueAfter.sub(vaultValueBefore)).to.gt(parseUnits("-1")); // Check the strategy LP balance decreased - const strategyLpBalanceAfter = await cvxRewardPool.balanceOf( + const strategyLpBalanceAfter = await swapXGauge.balanceOf( swapXAMOStrategy.address ); expect(strategyLpBalanceBefore.sub(strategyLpBalanceAfter)).to.eq(lpAmount); @@ -746,61 +725,60 @@ async function assertRemoveOnlyAssets(lpAmount, fixture) { async function calcOSMintAmount(fixture, wsDepositAmount) { const { swapXPool } = fixture; - // Get the wS and OS balances in the pool + // Get the reserves of the pool const { _reserve0: wsReserves, _reserve1: osReserves } = await swapXPool.getReserves(); const osMintAmount = wsDepositAmount.mul(osReserves).div(wsReserves); log(`OS mint amount : ${formatUnits(osMintAmount)}`); - return { osMintAmount, wsReserves, osReserves }; + return { osMintAmount, reserves: { ws: wsReserves, os: osReserves } }; } // Calculate the amount of OS burnt from a withdraw -async function calcOethWithdrawAmount(fixture, wethWithdrawAmount) { +async function calcOSWithdrawAmount(fixture, wethWithdrawAmount) { const { swapXPool } = fixture; - // Get the ETH and OS balances in the Curve pool - const curveBalances = await swapXPool.get_balances(); + // Get the reserves of the pool + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); - // OS to burn = WETH withdrawn * OS pool balance / ETH pool balance - const oethBurnAmount = wethWithdrawAmount - .mul(curveBalances[1]) - .div(curveBalances[0]); + // OS to burn = wS withdrawn * OS reserves / wS reserves + const osBurnAmount = wethWithdrawAmount.mul(osReserves).div(wsReserves); - log(`OS burn amount : ${formatUnits(oethBurnAmount)}`); + log(`OS burn amount : ${formatUnits(osBurnAmount)}`); - return { oethBurnAmount, curveBalances }; + return { osBurnAmount, reserves: { ws: wsReserves, os: osReserves } }; } -// Calculate the OS and ETH amounts from a withdrawAll +// Calculate the OS and wS amounts from a withdrawAll async function calcWithdrawAllAmounts(fixture) { - const { swapXAMOStrategy, cvxRewardPool, swapXPool } = fixture; + const { swapXAMOStrategy, swapXGauge, swapXPool } = fixture; - // Get the ETH and OS balances in the Curve pool - const curveBalances = await swapXPool.get_balances(); - const strategyLpAmount = await cvxRewardPool.balanceOf( - swapXAMOStrategy.address - ); + // Get the reserves of the pool + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + // const curveBalances = await swapXPool.get_balances(); + const strategyLpAmount = await swapXGauge.balanceOf(swapXAMOStrategy.address); const totalLpSupply = await swapXPool.totalSupply(); + // wS to withdraw = wS pool balance * strategy LP amount / total pool LP amount + const wsWithdrawAmount = wsReserves.mul(strategyLpAmount).div(totalLpSupply); // OS to burn = OS pool balance * strategy LP amount / total pool LP amount - const oethBurnAmount = curveBalances[1] - .mul(strategyLpAmount) - .div(totalLpSupply); - // ETH to withdraw = ETH pool balance * strategy LP amount / total pool LP amount - const ethWithdrawAmount = curveBalances[0] - .mul(strategyLpAmount) - .div(totalLpSupply); - - log(`OS burn amount : ${formatUnits(oethBurnAmount)}`); - log(`ETH withdraw amount : ${formatUnits(ethWithdrawAmount)}`); - - return { oethBurnAmount, ethWithdrawAmount, curveBalances }; + const osBurnAmount = osReserves.mul(strategyLpAmount).div(totalLpSupply); + + log(`wS withdraw amount : ${formatUnits(wsWithdrawAmount)}`); + log(`OS burn amount : ${formatUnits(osBurnAmount)}`); + + return { + wsWithdrawAmount, + osBurnAmount, + reserves: { ws: wsReserves, os: osReserves }, + }; } // Calculate the amount of OS burned from a removeAndBurnOTokens -async function calcOethRemoveAmount(fixture, lpAmount) { +async function calcOSRemoveAmount(fixture, lpAmount) { const { oethGaugeSigner, swapXPool } = fixture; // Static call to get the OS removed from the pool for a given amount of LP tokens @@ -817,8 +795,8 @@ async function calcOethRemoveAmount(fixture, lpAmount) { return oethBurnAmount; } -// Calculate the amount of ETH burned from a removeOnlyAssets -async function calcEthRemoveAmount(fixture, lpAmount) { +// Calculate the amount of wS burned from a removeOnlyAssets +async function calcWSRemoveAmount(fixture, lpAmount) { const { swapXPool } = fixture; // Get the ETH removed from the pool for a given amount of LP tokens From 95939827bc5496c7958f31c199f17f7dc5824f7d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 26 Feb 2025 15:34:33 +1100 Subject: [PATCH 39/80] Used snapData and assertChangedData in SwapX AMO fork tests --- .../contracts/interfaces/sonic/ISwapXPair.sol | 22 ++ .../sonic/swapx-amo.sonic.fork-test.js | 228 ++++++++++-------- 2 files changed, 146 insertions(+), 104 deletions(-) diff --git a/contracts/contracts/interfaces/sonic/ISwapXPair.sol b/contracts/contracts/interfaces/sonic/ISwapXPair.sol index b4a7ea5f4c..9f3250bebc 100644 --- a/contracts/contracts/interfaces/sonic/ISwapXPair.sol +++ b/contracts/contracts/interfaces/sonic/ISwapXPair.sol @@ -5,6 +5,28 @@ interface IPair { event Approval(address indexed src, address indexed guy, uint256 wad); event Transfer(address indexed src, address indexed dst, uint256 wad); + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn( + address indexed sender, + uint256 amount0, + uint256 amount1, + address indexed to + ); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Claim( + address indexed sender, + address indexed recipient, + uint256 amount0, + uint256 amount1 + ); + function metadata() external view diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index e2fdab04cf..592e8501f9 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -83,6 +83,84 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); + const snapData = async (fixture) => { + const { oSonicVault, swapXAMOStrategy, oSonic, swapXPool, swapXGauge, wS } = + fixture; + + const stratBalance = await swapXAMOStrategy.checkBalance(wS.address); + const osSupply = await oSonic.totalSupply(); + const poolSupply = await swapXPool.totalSupply(); + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + const stratGaugeBalance = await swapXGauge.balanceOf( + swapXAMOStrategy.address + ); + const vaultWSBalance = await wS.balanceOf(oSonicVault.address); + + return { + stratBalance, + osSupply, + poolSupply, + reserves: { ws: wsReserves, os: osReserves }, + stratGaugeBalance, + vaultWSBalance, + }; + }; + + const assertChangedData = async (dataBefore, delta, fixture) => { + const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = + fixture; + + const expectedStratBalance = dataBefore.stratBalance.add( + delta.stratBalance + ); + expect( + await swapXAMOStrategy.checkBalance(wS.address), + "Strategy's check balance" + ).to.withinRange(expectedStratBalance.sub(1), expectedStratBalance); + expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( + dataBefore.osSupply.add(delta.osSupply) + ); + + // Check the pool's reserves + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + expect(wsReserves, "wS reserves").to.equal( + dataBefore.reserves.ws.add(delta.reserves.ws) + ); + expect(osReserves, "OS reserves").to.equal( + dataBefore.reserves.os.add(delta.reserves.os) + ); + + // Check the strategy's gauge balance + // Calculate the liquidity added to the pool + const wsLiquidity = delta.reserves.ws + .mul(dataBefore.poolSupply) + .div(dataBefore.reserves.ws); + const osLiquidity = delta.reserves.os + .mul(dataBefore.poolSupply) + .div(dataBefore.reserves.os); + const deltaStratGaugeBalance = wsLiquidity.lt(osLiquidity) + ? wsLiquidity + : osLiquidity; + const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( + deltaStratGaugeBalance + ); + expect( + await swapXGauge.balanceOf(swapXAMOStrategy.address), + "Strategy's gauge balance" + ).to.withinRange( + expectedStratGaugeBalance.sub(1), + expectedStratGaugeBalance.add(1) + ); + + // Check Vault's wS balance + expect( + await wS.balanceOf(oSonicVault.address), + "Vault's wS balance" + ).to.equal(dataBefore.vaultWSBalance.add(delta.vaultWSBalance)); + }; + describe("with some wS in the vault", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, @@ -93,28 +171,13 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Vault should deposit some wS to AMO strategy", async function () { - const { - swapXAMOStrategy, - oSonic, - swapXPool, - swapXGauge, - oSonicVaultSigner, - wS, - } = fixture; + const { swapXAMOStrategy, oSonic, swapXPool, oSonicVaultSigner, wS } = + fixture; - const wsDepositAmount = await units("5000", wS); + const dataBefore = await snapData(fixture); - const stratBalanceBefore = await swapXAMOStrategy.checkBalance( - wS.address - ); - const osSupplyBefore = await oSonic.totalSupply(); - const { osMintAmount, reserves: reservesBefore } = await calcOSMintAmount( - fixture, - wsDepositAmount - ); - const stratGaugeBalanceBefore = await swapXGauge.balanceOf( - swapXAMOStrategy.address - ); + const wsDepositAmount = await units("5000", wS); + const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); // Vault transfers wS to strategy await wS @@ -133,29 +196,15 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .to.emit(swapXAMOStrategy, "Deposit") .withArgs(oSonic.address, swapXPool.address, osMintAmount); - // Check the strategy balance increased - const expectedStratBalance = stratBalanceBefore - .add(wsDepositAmount) - .add(osMintAmount); - expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( - expectedStratBalance.sub(1), - expectedStratBalance - ); - - // Check the OS total supply increase - expect(await oSonic.totalSupply()).to.equal( - osSupplyBefore.add(osMintAmount) - ); - - // Check the reserves in the pool - const { _reserve0: wsReservesAfter, _reserve1: osReservesAfter } = - await swapXPool.getReserves(); - expect(wsReservesAfter).to.equal(reservesBefore.ws.add(wsDepositAmount)); - expect(osReservesAfter).to.equal(reservesBefore.os.add(osMintAmount)); - - // Check the strategy's gauge balance increased - expect(await swapXGauge.balanceOf(swapXAMOStrategy.address)).to.gt( - stratGaugeBalanceBefore + await assertChangedData( + dataBefore, + { + stratBalance: wsDepositAmount.add(osMintAmount).sub(1), + osSupply: osMintAmount, + reserves: { ws: wsDepositAmount, os: osMintAmount }, + vaultWSBalance: wsDepositAmount.mul(-1), + }, + fixture ); }); it("Only vault can deposit some wS to AMO strategy", async function () { @@ -223,16 +272,11 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const { swapXAMOStrategy, swapXPool, oSonic, oSonicVaultSigner, wS } = fixture; - const stratBalanceBefore = await swapXAMOStrategy.checkBalance( - wS.address - ); - const { - osBurnAmount, - wsWithdrawAmount, - reserves: reservesBefore, - } = await calcWithdrawAllAmounts(fixture); + const dataBefore = await snapData(fixture); - const osSupplyBefore = await oSonic.totalSupply(); + const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts( + fixture + ); // Now try to withdraw all the wS from the strategy const tx = await swapXAMOStrategy @@ -247,24 +291,15 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .to.emit(swapXAMOStrategy, "Withdrawal") .withArgs(oSonic.address, swapXPool.address, osBurnAmount); - // Check the strategy balance decreased - const expectedStratBalance = stratBalanceBefore - .sub(wsWithdrawAmount) - .sub(osBurnAmount); - expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( - expectedStratBalance.sub(1), - expectedStratBalance - ); - - // Check the wS and OS reserves in the pool - const { _reserve0: wsReservesAfter, _reserve1: osReservesAfter } = - await swapXPool.getReserves(); - expect(wsReservesAfter).to.equal(reservesBefore.ws.sub(wsWithdrawAmount)); - expect(osReservesAfter).to.equal(reservesBefore.os.sub(osBurnAmount)); - - // Check the OS total supply decrease - expect(await oSonic.totalSupply()).to.equal( - osSupplyBefore.sub(osBurnAmount) + await assertChangedData( + dataBefore, + { + stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), + osSupply: osBurnAmount.mul(-1), + reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, + vaultWSBalance: wsWithdrawAmount, + }, + fixture ); }); it("Vault should be able to withdraw some", async () => { @@ -277,15 +312,13 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { wS, } = fixture; - const wsWithdrawAmount = oethUnits("1000"); + const dataBefore = await snapData(fixture); - const stratBalanceBefore = await swapXAMOStrategy.checkBalance( - wS.address + const wsWithdrawAmount = oethUnits("1000"); + const osBurnAmount = await calcOSWithdrawAmount( + fixture, + wsWithdrawAmount ); - const { osBurnAmount, reserves: reservesBefore } = - await calcOSWithdrawAmount(fixture, wsWithdrawAmount); - const osSupplyBefore = await oSonic.totalSupply(); - const vaultWSBalanceBefore = await wS.balanceOf(oSonicVault.address); // Now try to withdraw the wS from the strategy const tx = await swapXAMOStrategy @@ -301,29 +334,18 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { _pToken: swapXPool.address, }); - // Check the strategy balance decreased - const expectedStratBalance = stratBalanceBefore - .sub(wsWithdrawAmount) - .sub(osBurnAmount); - expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( - expectedStratBalance.sub(1), - expectedStratBalance - ); - - // Check the wS and OS reserves in the pool - const { _reserve0: wsReservesAfter, _reserve1: osReservesAfter } = - await swapXPool.getReserves(); - expect(wsReservesAfter).to.equal(reservesBefore.ws.sub(wsWithdrawAmount)); - expect(osReservesAfter).to.equal(reservesBefore.os.sub(osBurnAmount)); - - // Check the OS total supply decrease - expect(await oSonic.totalSupply()).to.equal( - osSupplyBefore.sub(osBurnAmount) - ); - - // Check the wS balance in the Vault - expect(await wS.balanceOf(oSonicVault.address)).to.equal( - vaultWSBalanceBefore.add(wsWithdrawAmount) + await assertChangedData( + dataBefore, + { + stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), + osSupply: osBurnAmount.add(1).mul(-1), + reserves: { + ws: wsWithdrawAmount.mul(-1), + os: osBurnAmount.add(1).mul(-1), + }, + vaultWSBalance: wsWithdrawAmount, + }, + fixture ); }); it("Only vault can withdraw some WETH from AMO strategy", async function () { @@ -732,7 +754,7 @@ async function calcOSMintAmount(fixture, wsDepositAmount) { const osMintAmount = wsDepositAmount.mul(osReserves).div(wsReserves); log(`OS mint amount : ${formatUnits(osMintAmount)}`); - return { osMintAmount, reserves: { ws: wsReserves, os: osReserves } }; + return osMintAmount; } // Calculate the amount of OS burnt from a withdraw @@ -748,7 +770,7 @@ async function calcOSWithdrawAmount(fixture, wethWithdrawAmount) { log(`OS burn amount : ${formatUnits(osBurnAmount)}`); - return { osBurnAmount, reserves: { ws: wsReserves, os: osReserves } }; + return osBurnAmount; } // Calculate the OS and wS amounts from a withdrawAll @@ -758,7 +780,6 @@ async function calcWithdrawAllAmounts(fixture) { // Get the reserves of the pool const { _reserve0: wsReserves, _reserve1: osReserves } = await swapXPool.getReserves(); - // const curveBalances = await swapXPool.get_balances(); const strategyLpAmount = await swapXGauge.balanceOf(swapXAMOStrategy.address); const totalLpSupply = await swapXPool.totalSupply(); @@ -773,7 +794,6 @@ async function calcWithdrawAllAmounts(fixture) { return { wsWithdrawAmount, osBurnAmount, - reserves: { ws: wsReserves, os: osReserves }, }; } From 7bbe2284a2b3c1199dffc92c3cd05781dc2b9e74 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 27 Feb 2025 05:40:38 +1100 Subject: [PATCH 40/80] Config of gauge --- contracts/deploy/sonic/011_swapx_amo.js | 32 ------------------------- contracts/utils/addresses.js | 3 +-- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/contracts/deploy/sonic/011_swapx_amo.js b/contracts/deploy/sonic/011_swapx_amo.js index 89d92cb012..5081e7bd4d 100644 --- a/contracts/deploy/sonic/011_swapx_amo.js +++ b/contracts/deploy/sonic/011_swapx_amo.js @@ -33,38 +33,6 @@ module.exports = deployOnSonic( "SonicSwapXAMOStrategyProxy" ); - const swapXVoter = await ethers.getContractAt( - "IVoterV3", - addresses.sonic.SwapXVoter - ); - - const gaugeAddress = await swapXVoter.gauges( - addresses.sonic.SwapXWSOS.pool - ); - console.log( - `Gauge for the wS/OS pool ${ - addresses.sonic.SwapXWSOS.pool - } is ${await swapXVoter.gauges(addresses.sonic.SwapXWSOS.pool)}` - ); - - if (gaugeAddress === addresses.zero) { - // Create the wS/OS Gauge - const swapXOwnerSigner = await impersonateAndFund( - addresses.sonic.SwapXOwner - ); - - console.log( - `Creating gauge for the wS/OS pool ${ - addresses.sonic.SwapXWSOS.pool - } using signer ${await swapXOwnerSigner.getAddress()}` - ); - await swapXVoter - .connect(swapXOwnerSigner) - .createGauge(addresses.sonic.SwapXWSOS.pool, 0); - } else { - addresses.sonic.SwapXWSOS.gauge = gaugeAddress; - } - // Deploy Sonic SwapX AMO Strategy implementation const dSonicSwapXAMOStrategy = await deployWithConfirmation( "SonicSwapXAMOStrategy", diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 9fe490ab8b..259fde38e0 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -406,8 +406,7 @@ addresses.sonic.SwapXOsGEMSx.pool = addresses.sonic.SwapXWSOS = {}; addresses.sonic.SwapXWSOS.pool = "0xcfE67b6c7B65c8d038e666b3241a161888B7f2b0"; -// TODO replace once the gauge is deployed -addresses.sonic.SwapXWSOS.gauge = "0x2E5228cFEcFEFA439f92Aa4823E5593de5bFcB7D"; +addresses.sonic.SwapXWSOS.gauge = "0x083D761B2A3e1fb5914FA61c6Bf11A93dcb60709"; addresses.sonic.SwapXOsUSDCeMultisigBooster = "0x4636269e7CDc253F6B0B210215C3601558FE80F6"; From 0d78b3c4494846e325f3a2be44722cf04d14b802 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 27 Feb 2025 05:42:01 +1100 Subject: [PATCH 41/80] More fork tests --- .../contracts/interfaces/sonic/ISwapXPair.sol | 2 + .../sonic/SonicSwapXAMOStrategy.sol | 6 + contracts/deploy/sonic/011_swapx_amo.js | 1 - contracts/test/_fixture-sonic.js | 44 +++- .../sonic/swapx-amo.sonic.fork-test.js | 243 ++++++++---------- 5 files changed, 154 insertions(+), 142 deletions(-) diff --git a/contracts/contracts/interfaces/sonic/ISwapXPair.sol b/contracts/contracts/interfaces/sonic/ISwapXPair.sol index 9f3250bebc..347957bc33 100644 --- a/contracts/contracts/interfaces/sonic/ISwapXPair.sol +++ b/contracts/contracts/interfaces/sonic/ISwapXPair.sol @@ -117,4 +117,6 @@ interface IPair { function isStable() external view returns (bool); function skim(address to) external; + + function sync() external; } diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 73abe02c11..318aa27285 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -417,7 +417,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IGauge(gauge).deposit(lpTokens); } + /// @param lpTokens Amount of SwapX pool LP tokens to withdraw from the gauge function _withdrawFromGaugeAndPool(uint256 lpTokens) internal { + require( + IGauge(gauge).balanceOf(address(this)) >= lpTokens, + "Not enough LP tokens in gauge" + ); + // Withdraw pool LP tokens from the gauge IGauge(gauge).withdraw(lpTokens); diff --git a/contracts/deploy/sonic/011_swapx_amo.js b/contracts/deploy/sonic/011_swapx_amo.js index 5081e7bd4d..9441066619 100644 --- a/contracts/deploy/sonic/011_swapx_amo.js +++ b/contracts/deploy/sonic/011_swapx_amo.js @@ -4,7 +4,6 @@ const { withConfirmation, } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); -const { impersonateAndFund } = require("../../utils/signers"); module.exports = deployOnSonic( { diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index e3af57b47f..35f3bfb8b9 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -199,7 +199,7 @@ const defaultSonicFixture = deployments.createFixture(async () => { await wS.connect(user).deposit({ value: oethUnits("10000000") }); // Set allowance on the vault - await wS.connect(user).approve(oSonicVault.address, oethUnits("5000")); + await wS.connect(user).approve(oSonicVault.address, oethUnits("100000000")); } return { @@ -257,7 +257,8 @@ async function swapXAMOFixture( ) { const fixture = await defaultSonicFixture(); - const { oSonicVault, nick, strategist, timelock, wS } = fixture; + const { oSonic, oSonicVault, rafael, nick, strategist, timelock, wS } = + fixture; let swapXAMOStrategy, swapXPool, swapXGauge; @@ -316,6 +317,45 @@ async function swapXAMOFixture( } } + if (config?.balancePool) { + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + + const diff = parseInt( + wsReserves.sub(osReserves).div(oethUnits("1")).toString() + ); + + if (diff > 0) { + config.poolAddOSAmount = (config.poolAddOSAmount || 0) + diff; + } else if (diff < 0) { + config.poolAddwSAmount = (config.poolAddwSAmount || 0) - diff; + } + } + + // Add wS to the pool + if (config?.poolAddwSAmount > 0) { + log(`Adding ${config.poolAddwSAmount} wS to the pool`); + // transfer wS to the pool + const wsAmount = parseUnits(config.poolAddwSAmount.toString(), 18); + await wS.connect(nick).transfer(swapXPool.address, wsAmount); + } + + // Add OS to the pool + if (config?.poolAddOSAmount > 0) { + log(`Adding ${config.poolAddOSAmount} OS to the pool`); + + const osAmount = parseUnits(config.poolAddOSAmount.toString(), 18); + + // Mint OS with wS + await oSonicVault.connect(rafael).mint(wS.address, osAmount, 0); + + // transfer OS to the pool + await oSonic.connect(rafael).transfer(swapXPool.address, osAmount); + } + + // force reserves to match balances + await swapXPool.sync(); + return { ...fixture, swapXAMOStrategy, swapXPool, swapXGauge }; } diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 592e8501f9..9ad510f925 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -83,84 +83,6 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - const snapData = async (fixture) => { - const { oSonicVault, swapXAMOStrategy, oSonic, swapXPool, swapXGauge, wS } = - fixture; - - const stratBalance = await swapXAMOStrategy.checkBalance(wS.address); - const osSupply = await oSonic.totalSupply(); - const poolSupply = await swapXPool.totalSupply(); - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - const stratGaugeBalance = await swapXGauge.balanceOf( - swapXAMOStrategy.address - ); - const vaultWSBalance = await wS.balanceOf(oSonicVault.address); - - return { - stratBalance, - osSupply, - poolSupply, - reserves: { ws: wsReserves, os: osReserves }, - stratGaugeBalance, - vaultWSBalance, - }; - }; - - const assertChangedData = async (dataBefore, delta, fixture) => { - const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = - fixture; - - const expectedStratBalance = dataBefore.stratBalance.add( - delta.stratBalance - ); - expect( - await swapXAMOStrategy.checkBalance(wS.address), - "Strategy's check balance" - ).to.withinRange(expectedStratBalance.sub(1), expectedStratBalance); - expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( - dataBefore.osSupply.add(delta.osSupply) - ); - - // Check the pool's reserves - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - expect(wsReserves, "wS reserves").to.equal( - dataBefore.reserves.ws.add(delta.reserves.ws) - ); - expect(osReserves, "OS reserves").to.equal( - dataBefore.reserves.os.add(delta.reserves.os) - ); - - // Check the strategy's gauge balance - // Calculate the liquidity added to the pool - const wsLiquidity = delta.reserves.ws - .mul(dataBefore.poolSupply) - .div(dataBefore.reserves.ws); - const osLiquidity = delta.reserves.os - .mul(dataBefore.poolSupply) - .div(dataBefore.reserves.os); - const deltaStratGaugeBalance = wsLiquidity.lt(osLiquidity) - ? wsLiquidity - : osLiquidity; - const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( - deltaStratGaugeBalance - ); - expect( - await swapXGauge.balanceOf(swapXAMOStrategy.address), - "Strategy's gauge balance" - ).to.withinRange( - expectedStratGaugeBalance.sub(1), - expectedStratGaugeBalance.add(1) - ); - - // Check Vault's wS balance - expect( - await wS.balanceOf(oSonicVault.address), - "Vault's wS balance" - ).to.equal(dataBefore.vaultWSBalance.add(delta.vaultWSBalance)); - }; - describe("with some wS in the vault", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, @@ -375,18 +297,18 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - describe.skip("with a lot more OS in the pool", () => { + describe("with a lot more OS in the pool", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, - depositToStrategy: false, - poolAddOethAmount: 60000, + depositToStrategy: true, + poolAddOSAmount: 60000, balancePool: true, }); beforeEach(async () => { fixture = await loadFixture(); }); - it("Strategist should remove a little OS from the pool", async () => { - await assertRemoveAndBurn(parseUnits("3"), fixture); + it("Strategist should swap assets to the pool", async () => { + await assertSwapAssetsToPool(parseUnits("3"), fixture); }); it("Strategist should remove a lot of OS from the pool", async () => { const { swapXGauge, swapXAMOStrategy } = fixture; @@ -397,7 +319,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .mul(99) .div(100); - await assertRemoveAndBurn(lpAmount, fixture); + await assertSwapAssetsToPool(lpAmount, fixture); }); it("Strategist should fail to add even more OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; @@ -570,51 +492,112 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); -async function assertRemoveAndBurn(lpAmount, fixture) { - const { swapXAMOStrategy, swapXPool, oSonic, strategist } = fixture; +const snapData = async (fixture) => { + const { oSonicVault, swapXAMOStrategy, oSonic, swapXPool, swapXGauge, wS } = + fixture; - const oethBurnAmount = await calcOSRemoveAmount(fixture, lpAmount); - const curveBalancesBefore = await swapXPool.get_balances(); - const osSupplyBefore = await oSonic.totalSupply(); + const stratBalance = await swapXAMOStrategy.checkBalance(wS.address); + const osSupply = await oSonic.totalSupply(); + const poolSupply = await swapXPool.totalSupply(); + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + const stratGaugeBalance = await swapXGauge.balanceOf( + swapXAMOStrategy.address + ); + const vaultWSBalance = await wS.balanceOf(oSonicVault.address); + const stratWSBalance = await wS.balanceOf(swapXAMOStrategy.address); - log(`Before remove and burn of ${formatUnits(lpAmount)} OS from the pool`); - await run("amoStrat", { - pool: "OS", - output: false, - }); + return { + stratBalance, + osSupply, + poolSupply, + reserves: { ws: wsReserves, os: osReserves }, + stratGaugeBalance, + vaultWSBalance, + stratWSBalance, + }; +}; + +const assertChangedData = async (dataBefore, delta, fixture) => { + const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = + fixture; + + const expectedStratBalance = dataBefore.stratBalance.add(delta.stratBalance); + expect( + await swapXAMOStrategy.checkBalance(wS.address), + "Strategy's check balance" + ).to.withinRange(expectedStratBalance.sub(1), expectedStratBalance); + expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( + dataBefore.osSupply.add(delta.osSupply) + ); - // Remove and burn OS from the pool - const tx = await swapXAMOStrategy - .connect(strategist) - .removeAndBurnOTokens(lpAmount); + // Check the pool's reserves + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + expect(wsReserves, "wS reserves").to.equal( + dataBefore.reserves.ws.add(delta.reserves.ws) + ); + expect(osReserves, "OS reserves").to.equal( + dataBefore.reserves.os.add(delta.reserves.os) + ); - const receipt = await tx.wait(); + // Check the strategy's gauge balance + // Calculate the liquidity added to the pool + const wsLiquidity = delta.reserves.ws + .mul(dataBefore.poolSupply) + .div(dataBefore.reserves.ws); + const osLiquidity = delta.reserves.os + .mul(dataBefore.poolSupply) + .div(dataBefore.reserves.os); + const deltaStratGaugeBalance = wsLiquidity.lt(osLiquidity) + ? wsLiquidity + : osLiquidity; + const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( + deltaStratGaugeBalance + ); + expect( + await swapXGauge.balanceOf(swapXAMOStrategy.address), + "Strategy's gauge balance" + ).to.withinRange( + expectedStratGaugeBalance.sub(1), + expectedStratGaugeBalance.add(1) + ); - log("After remove and burn of OS from pool"); - await run("amoStrat", { - pool: "OS", - output: false, - fromBlock: receipt.blockNumber - 1, - }); + // Check Vault's wS balance + expect( + await wS.balanceOf(oSonicVault.address), + "Vault's wS balance" + ).to.equal(dataBefore.vaultWSBalance.add(delta.vaultWSBalance)); +}; - // Check emitted event - await expect(tx) - .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(oSonic.address, swapXPool.address, oethBurnAmount); +async function assertSwapAssetsToPool(wsAmount, fixture) { + const { swapXAMOStrategy, strategist } = fixture; - // Check the ETH and OS balances in the Curve pool - const curveBalancesAfter = await swapXPool.get_balances(); - expect(curveBalancesAfter[0]).to.equal(curveBalancesBefore[0]); - expect(curveBalancesAfter[1]).to.approxEqualTolerance( - curveBalancesBefore[1].sub(oethBurnAmount), - 0.01 // 0.01% - ); + const dataBefore = await snapData(fixture); - // Check the OS total supply decrease - expect(await oSonic.totalSupply()).to.approxEqualTolerance( - osSupplyBefore.sub(oethBurnAmount), - 0.01 // 0.01% or 1 basis point - ); + log(`wS in the strategy : ${formatUnits(dataBefore.stratWSBalance)}`); + + // Swap wS to the pool and burn the received OS from the pool + const tx = await swapXAMOStrategy + .connect(strategist) + .swapAssetsToPool(wsAmount); + + // Check emitted event + // const expectedLpAmount = BigNumber.from(0); + // const expectedOSBurnAmount = BigNumber.from(0); + await expect(tx).to.emit(swapXAMOStrategy, "SwapAssetsToPool"); + // .withArgs(wsAmount, expectedLpAmount, expectedOSBurnAmount); + + // await assertChangedData( + // dataBefore, + // { + // stratBalance: wsAmount.add(expectedOSBurnAmount).sub(1), + // osSupply: expectedOSBurnAmount.mul(-1), + // reserves: { ws: wsAmount.mul(-1), os: expectedOSBurnAmount.mul(-1) }, + // vaultWSBalance: BigNumber.from(0), + // }, + // fixture + // ); } async function assertMintAndAddOTokens(oethMintAmount, fixture) { @@ -797,24 +780,6 @@ async function calcWithdrawAllAmounts(fixture) { }; } -// Calculate the amount of OS burned from a removeAndBurnOTokens -async function calcOSRemoveAmount(fixture, lpAmount) { - const { oethGaugeSigner, swapXPool } = fixture; - - // Static call to get the OS removed from the pool for a given amount of LP tokens - const oethBurnAmount = await swapXPool - .connect(oethGaugeSigner) - .callStatic["remove_liquidity_one_coin(uint256,int128,uint256)"]( - lpAmount, - 1, - 0 - ); - - log(`OS burn amount : ${formatUnits(oethBurnAmount)}`); - - return oethBurnAmount; -} - // Calculate the amount of wS burned from a removeOnlyAssets async function calcWSRemoveAmount(fixture, lpAmount) { const { swapXPool } = fixture; From 767d59870339d50cd9d70d027b41cd80b0b9d6db Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 27 Feb 2025 11:44:14 +1100 Subject: [PATCH 42/80] More SwapX AMO fork tests --- .../sonic/SonicSwapXAMOStrategy.sol | 31 +++++---------- .../sonic/swapx-amo.sonic.fork-test.js | 38 ++++--------------- 2 files changed, 17 insertions(+), 52 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 318aa27285..4e4b57454c 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -109,20 +109,6 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { } } - /// @dev Checks that the strategy value has not decreased by more than a dust amount - modifier strategyValueChecker() { - // Get the strategy value before the call - uint256 balanceBefore = checkBalance(ws); - - _; - - // Get the strategy value after the call - uint256 balanceAfter = checkBalance(ws); - - // The strategy value should not decrease by more than a dust amount - require(balanceAfter >= balanceBefore - 10, "Strategy value decreased"); - } - constructor( BaseStrategyConfig memory _baseConfig, address _os, @@ -236,6 +222,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { emit Withdrawal(_ws, pool, _wsAmount); + // Skim the pool in case extra tokens were added + IPair(pool).skim(address(this)); + // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back uint256 lpTokens = calcTokensToBurn(_wsAmount); @@ -300,10 +289,11 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { external onlyStrategist nonReentrant - // TODO which one is better to use here? improvePoolBalance - strategyValueChecker { + // Skim the pool in case extra tokens were added + IPair(pool).skim(address(this)); + // 1. Partially remove liquidity so there’s enough wS for the swap // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back @@ -335,10 +325,11 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { external onlyStrategist nonReentrant - // TODO which one is better to use here? improvePoolBalance - strategyValueChecker { + // Skim the pool in case extra tokens were added + IPair(pool).skim(address(this)); + // 1. Mint OS so it can be swapped into the pool // There shouldn't be any OS in the strategy but just in case @@ -378,11 +369,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { function calcTokensToBurn(uint256 _wsAmount) internal + view returns (uint256 lpTokens) { - // Skim the pool in case extra wS tokens were added - IPair(pool).skim(address(this)); - /* The rate between coins in the pool determines the rate at which pool returns * tokens when doing balanced removal (remove_liquidity call). And by knowing how much wS * we want we can determine how much of OS we receive by removing liquidity. diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 9ad510f925..35894c7f4c 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -311,23 +311,15 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await assertSwapAssetsToPool(parseUnits("3"), fixture); }); it("Strategist should remove a lot of OS from the pool", async () => { - const { swapXGauge, swapXAMOStrategy } = fixture; - const lpBalance = await swapXGauge.balanceOf(swapXAMOStrategy.address); - - const lpAmount = lpBalance - // reduce by 1% - .mul(99) - .div(100); - - await assertSwapAssetsToPool(lpAmount, fixture); + await assertSwapAssetsToPool(parseUnits("3000"), fixture); }); - it("Strategist should fail to add even more OS to the pool", async () => { + it.skip("Strategist should fail to add even more OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; - // Mint and add OS to the pool + // try swapping OS into the pool const tx = swapXAMOStrategy .connect(strategist) - .mintAndAddOTokens(parseUnits("1")); + .swapOTokensToPool(parseUnits("1")); await expect(tx).to.be.revertedWith("OTokens balance worse"); }); @@ -573,31 +565,15 @@ const assertChangedData = async (dataBefore, delta, fixture) => { async function assertSwapAssetsToPool(wsAmount, fixture) { const { swapXAMOStrategy, strategist } = fixture; - const dataBefore = await snapData(fixture); - - log(`wS in the strategy : ${formatUnits(dataBefore.stratWSBalance)}`); - // Swap wS to the pool and burn the received OS from the pool const tx = await swapXAMOStrategy .connect(strategist) .swapAssetsToPool(wsAmount); // Check emitted event - // const expectedLpAmount = BigNumber.from(0); - // const expectedOSBurnAmount = BigNumber.from(0); - await expect(tx).to.emit(swapXAMOStrategy, "SwapAssetsToPool"); - // .withArgs(wsAmount, expectedLpAmount, expectedOSBurnAmount); - - // await assertChangedData( - // dataBefore, - // { - // stratBalance: wsAmount.add(expectedOSBurnAmount).sub(1), - // osSupply: expectedOSBurnAmount.mul(-1), - // reserves: { ws: wsAmount.mul(-1), os: expectedOSBurnAmount.mul(-1) }, - // vaultWSBalance: BigNumber.from(0), - // }, - // fixture - // ); + await expect(tx) + .to.emit(swapXAMOStrategy, "SwapAssetsToPool") + .withNamedArgs({ _wsAmount: wsAmount }); } async function assertMintAndAddOTokens(oethMintAmount, fixture) { From e915cdbf5bde90cbb2331395ca540a85d837fd2d Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 3 Mar 2025 13:41:56 +1100 Subject: [PATCH 43/80] Fixed _swapExactTokensForTokens swapping OS in Use IERC20 for wS ERC20 functions --- .../sonic/SonicSwapXAMOStrategy.sol | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 4e4b57454c..ba51f4f2cc 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -12,7 +12,6 @@ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { StableMath } from "../../utils/StableMath.sol"; import { IVault } from "../../interfaces/IVault.sol"; -import { IWrappedSonic } from "../../interfaces/sonic/IWrappedSonic.sol"; import { IPair } from "../../interfaces/sonic/ISwapXPair.sol"; import { IGauge } from "../../interfaces/sonic/ISwapXGauge.sol"; @@ -195,7 +194,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * @notice Deposit the strategy's entire balance of Wrapped S (wS) into the pool */ function depositAll() external override onlyVault nonReentrant { - uint256 balance = IWrappedSonic(ws).balanceOf(address(this)); + uint256 balance = IERC20(ws).balanceOf(address(this)); if (balance > 0) { _deposit(ws, balance); } @@ -239,7 +238,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Transfer wS to the recipient require( - IWrappedSonic(ws).transfer(_recipient, _wsAmount), + IERC20(ws).transfer(_recipient, _wsAmount), "Transfer of wS not successful" ); @@ -266,9 +265,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Get the strategy contract's wS balance. // This includes all that was removed from the SwapX pool and // any that was sitting in the strategy contract before the removal. - uint256 wsBalance = IWrappedSonic(ws).balanceOf(address(this)); + uint256 wsBalance = IERC20(ws).balanceOf(address(this)); require( - IWrappedSonic(ws).transfer(vaultAddress, wsBalance), + IERC20(ws).transfer(vaultAddress, wsBalance), "Transfer of wS not successful" ); @@ -346,7 +345,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // 3. Add wS and OS back to the pool in proportion to the pool's reserves // The wS is from the swap and any wS that was sitting in the strategy - uint256 wsLiquidity = IWrappedSonic(ws).balanceOf(address(this)); + uint256 wsLiquidity = IERC20(ws).balanceOf(address(this)); // Calculate how much OS liquidity is required from the wS liquidity (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); uint256 osLiquidity = (wsLiquidity * osReserves) / wsReserves; @@ -387,7 +386,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { lpTokens = ((_wsAmount + 1) * IPair(pool).totalSupply()) / - IWrappedSonic(ws).balanceOf(pool); + IERC20(ws).balanceOf(pool); } function _depositToPoolAndGauge(uint256 _wsAmount, uint256 osAmount) @@ -395,7 +394,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { returns (uint256 lpTokens) { // Transfer wS to the pool - IWrappedSonic(ws).transfer(pool, _wsAmount); + IERC20(ws).transfer(pool, _wsAmount); // Transfer OS to the pool IERC20(os).transfer(pool, osAmount); @@ -428,7 +427,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { address _tokenOut ) internal { // Transfer in tokens to the pool - IWrappedSonic(ws).transfer(pool, _amountIn); + IERC20(_tokenIn).transfer(pool, _amountIn); // Calculate how much out tokens we get from the swap uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); @@ -504,7 +503,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { require(_asset == ws, "Unsupported asset"); // wS balance needed here for the balance check that happens from vault during depositing. - balance = IWrappedSonic(ws).balanceOf(address(this)); + balance = IERC20(ws).balanceOf(address(this)); uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); if (lpTokens == 0) return balance; @@ -555,7 +554,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Approve SwapX pool for wS (required for adding liquidity) // slither-disable-next-line unused-return - IWrappedSonic(ws).approve(platformAddress, type(uint256).max); + IERC20(ws).approve(platformAddress, type(uint256).max); // Approve SwapX gauge contract to transfer SwapX pool LP tokens // This is needed for deposits of SwapX pool LP tokens into the gauge. From 1a095663d7d9f97e775484d54abe3d2c655dd41c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 3 Mar 2025 15:14:56 +1100 Subject: [PATCH 44/80] Minor SonicSwapXAMOStrategy changes --- .../sonic/SonicSwapXAMOStrategy.sol | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index ba51f4f2cc..8a7d77ee4f 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -48,15 +48,15 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { address public immutable gauge; event SwapOTokensToPool( - uint256 osToMint, + uint256 osMinted, uint256 wsLiquidity, uint256 osLiquidity, uint256 lpTokens ); event SwapAssetsToPool( - uint256 _wsAmount, + uint256 wsSwapped, uint256 lpTokens, - uint256 osToBurn + uint256 osBurnt ); /** @@ -96,12 +96,12 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { if (diffBefore == 0) { require(diffAfter == 0, "Position balance is worsened"); } else if (diffBefore < 0) { - // If the pool was originally imbalanced in favor of OETH, then + // If the pool was originally imbalanced in favor of OS, then // we want to check that the pool is now more balanced require(diffAfter <= 0, "OTokens overshot peg"); require(diffBefore < diffAfter, "OTokens balance worse"); } else if (diffBefore > 0) { - // If the pool was originally imbalanced in favor of ETH, then + // If the pool was originally imbalanced in favor of wS, then // we want to check that the pool is now more balanced require(diffAfter >= 0, "Assets overshot peg"); require(diffAfter < diffBefore, "Assets balance worse"); @@ -302,7 +302,10 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { _withdrawFromGaugeAndPool(lpTokens); // 2. Swap wS for OS against the pool - _swapExactTokensForTokens(_wsAmount, ws, os); + // Get the wS balance in the strategy as it could be less than the wsAmount due to rounding + uint256 wsInStrategy = IERC20(ws).balanceOf(address(this)); + // Swap exact amount of wS for OS against the pool + _swapExactTokensForTokens(wsInStrategy, ws, os); // 3. Burn all the OS left in the strategy from the remove liquidity and swap uint256 osToBurn = IERC20(os).balanceOf(address(this)); @@ -311,9 +314,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Ensure solvency of the vault _solvencyAssert(); - // TODO Emit event with the _wsAmount, lpTokens and osToBurn - - emit SwapAssetsToPool(_wsAmount, lpTokens, osToBurn); + emit SwapAssetsToPool(wsInStrategy, lpTokens, osToBurn); } /// @notice Used when there is more wS than OS in the pool. From e720766bd6cde42fa91db664ec97195807a51826 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 3 Mar 2025 17:58:55 +1100 Subject: [PATCH 45/80] More SwapX AMO fork tests --- .../sonic/SonicSwapXAMOStrategy.sol | 10 +- contracts/test/_fixture-sonic.js | 2 +- .../sonic/swapx-amo.sonic.fork-test.js | 350 ++++++------------ 3 files changed, 115 insertions(+), 247 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 8a7d77ee4f..0bf1639185 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -98,12 +98,12 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { } else if (diffBefore < 0) { // If the pool was originally imbalanced in favor of OS, then // we want to check that the pool is now more balanced - require(diffAfter <= 0, "OTokens overshot peg"); + require(diffAfter <= 0, "Assets overshot peg"); require(diffBefore < diffAfter, "OTokens balance worse"); } else if (diffBefore > 0) { // If the pool was originally imbalanced in favor of wS, then // we want to check that the pool is now more balanced - require(diffAfter >= 0, "Assets overshot peg"); + require(diffAfter >= 0, "OTokens overshot peg"); require(diffAfter < diffBefore, "Assets balance worse"); } } @@ -290,6 +290,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { nonReentrant improvePoolBalance { + require(_wsAmount > 0, "Must swap some wS"); + // Skim the pool in case extra tokens were added IPair(pool).skim(address(this)); @@ -327,6 +329,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { nonReentrant improvePoolBalance { + require(_osAmount > 0, "Must swap some OS"); + // Skim the pool in case extra tokens were added IPair(pool).skim(address(this)); @@ -334,7 +338,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // There shouldn't be any OS in the strategy but just in case uint256 osInStrategy = IERC20(os).balanceOf(address(this)); - require(_osAmount > osInStrategy, "OS in strategy"); + require(_osAmount >= osInStrategy, "Too much OS in strategy"); uint256 osToMint = _osAmount - osInStrategy; // Mint the required OS tokens to the strategy diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 35f3bfb8b9..dacff1b18f 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -250,9 +250,9 @@ async function swapXAMOFixture( config = { wsMintAmount: 0, depositToStrategy: false, + balancePool: false, poolAddwSAmount: 0, poolAddOSAmount: 0, - balancePool: false, } ) { const fixture = await defaultSonicFixture(); diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 35894c7f4c..e5ff684828 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -1,10 +1,9 @@ const { expect } = require("chai"); const { formatUnits, parseUnits } = require("ethers/lib/utils"); -const { run } = require("hardhat"); const { createFixtureLoader } = require("../../_fixture"); const { swapXAMOFixture } = require("../../_fixture-sonic"); -const { units, oethUnits, isCI } = require("../../helpers"); +const { isCI } = require("../../helpers"); const addresses = require("../../../utils/addresses"); const log = require("../../../utils/logger")("test:fork:sonic:swapx:amo"); @@ -98,7 +97,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const dataBefore = await snapData(fixture); - const wsDepositAmount = await units("5000", wS); + const wsDepositAmount = await parseUnits("5000"); const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); // Vault transfers wS to strategy @@ -236,7 +235,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const dataBefore = await snapData(fixture); - const wsWithdrawAmount = oethUnits("1000"); + const wsWithdrawAmount = parseUnits("1000"); const osBurnAmount = await calcOSWithdrawAmount( fixture, wsWithdrawAmount @@ -260,10 +259,10 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { dataBefore, { stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), - osSupply: osBurnAmount.add(1).mul(-1), + osSupply: osBurnAmount.mul(-1), reserves: { ws: wsWithdrawAmount.mul(-1), - os: osBurnAmount.add(1).mul(-1), + os: osBurnAmount.mul(-1), }, vaultWSBalance: wsWithdrawAmount, }, @@ -301,187 +300,175 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: true, - poolAddOSAmount: 60000, balancePool: true, + poolAddOSAmount: 60000, }); beforeEach(async () => { fixture = await loadFixture(); }); - it("Strategist should swap assets to the pool", async () => { + it("Strategist should swap a little assets to the pool", async () => { await assertSwapAssetsToPool(parseUnits("3"), fixture); }); - it("Strategist should remove a lot of OS from the pool", async () => { + it("Strategist should swap a lot of assets to the pool", async () => { await assertSwapAssetsToPool(parseUnits("3000"), fixture); }); - it.skip("Strategist should fail to add even more OS to the pool", async () => { + it("Strategist should swap most of the wS owned by the strategy", async () => { + await assertSwapAssetsToPool(parseUnits("4500"), fixture); + }); + it("Strategist should fail to swap all wS owned by the strategy", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + const tx = swapXAMOStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("5000")); + + await expect(tx).to.be.revertedWith("Assets overshot peg"); + }); + it("Strategist should fail to add more wS than owned by the strategy", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + const tx = swapXAMOStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("20000")); + + await expect(tx).to.be.revertedWith("Not enough LP tokens in gauge"); + }); + it("Strategist should fail to add more OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; // try swapping OS into the pool const tx = swapXAMOStrategy .connect(strategist) - .swapOTokensToPool(parseUnits("1")); + .swapOTokensToPool(parseUnits("0.001")); await expect(tx).to.be.revertedWith("OTokens balance worse"); }); }); - describe.skip("with a lot more OS in the pool", () => { + describe("with a little more OS in the pool", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 5000, - depositToStrategy: false, - poolAddOethAmount: 6000, + wsMintAmount: 20000, + depositToStrategy: true, balancePool: true, + poolAddOSAmount: 500, }); beforeEach(async () => { fixture = await loadFixture(); }); - it("Strategist should fail to remove the little ETH from the pool", async () => { + it("Strategist should swap a little assets to the pool", async () => { + await assertSwapAssetsToPool(parseUnits("3"), fixture); + }); + it("Strategist should swap enough wS to get the pool close to balanced", async () => { + // just under half the extra OS amount + const osAmount = parseUnits("247"); + await assertSwapAssetsToPool(osAmount, fixture); + }); + it("Strategist should fail to add too much wS to the pool", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + // try swapping half the extra OS in the pool + const tx = swapXAMOStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("250")); + + await expect(tx).to.be.revertedWith("Assets overshot peg"); + }); + it("Strategist should fail to add zero wS to the pool", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + const tx = swapXAMOStrategy.connect(strategist).swapAssetsToPool(0); + + await expect(tx).to.be.revertedWith("Must swap some wS"); + }); + it("Strategist should fail to add more OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; - // Remove ETH form the pool + // try swapping OS into the pool const tx = swapXAMOStrategy .connect(strategist) - .removeOnlyAssets(parseUnits("1")); + .swapOTokensToPool(parseUnits("0.001")); await expect(tx).to.be.revertedWith("OTokens balance worse"); }); }); - describe.skip("with a lot more wS in the pool", () => { + describe("with a lot more wS in the pool", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, - depositToStrategy: false, - poolAddEthAmount: 200000, + depositToStrategy: true, balancePool: true, + poolAddwSAmount: 20000, }); beforeEach(async () => { fixture = await loadFixture(); }); it("Strategist should add a little OS to the pool", async () => { - const oethMintAmount = oethUnits("3"); - await assertMintAndAddOTokens(oethMintAmount, fixture); + const osAmount = parseUnits("0.3"); + await assertSwapOTokensToPool(osAmount, fixture); }); it("Strategist should add a lot of OS to the pool", async () => { - const oethMintAmount = oethUnits("150000"); - await assertMintAndAddOTokens(oethMintAmount, fixture); - }); - it("Strategist should add OS to balance the pool", async () => { - const { swapXPool } = fixture; - const curveBalances = await swapXPool.get_balances(); - const oethMintAmount = curveBalances[0] - .sub(curveBalances[1]) - // reduce by 0.001% - .mul(99999) - .div(100000); - - await assertMintAndAddOTokens(oethMintAmount, fixture); - }); - it("Strategist should remove a little ETH from the pool", async () => { - const lpAmount = parseUnits("2"); - await assertRemoveOnlyAssets(lpAmount, fixture); - }); - it("Strategist should remove a lot ETH from the pool", async () => { - const { swapXGauge, swapXAMOStrategy } = fixture; - const lpBalance = await swapXGauge.balanceOf(swapXAMOStrategy.address); - const lpAmount = lpBalance - // reduce by 1% - .mul(99) - .div(100); - - await assertRemoveOnlyAssets(lpAmount, fixture); + const osAmount = parseUnits("9000"); + await assertSwapOTokensToPool(osAmount, fixture); }); - }); + it("Strategist should fail to add more wS to the pool", async () => { + const { swapXAMOStrategy, strategist } = fixture; - describe.skip("with a little more wS in the pool", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 20000, - depositToStrategy: false, - poolAddEthAmount: 22000, - balancePool: true, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Strategist should remove ETH to balance the pool", async () => { - const { swapXGauge, swapXAMOStrategy } = fixture; - const lpBalance = await swapXGauge.balanceOf(swapXAMOStrategy.address); - const lpAmount = lpBalance - // reduce by 1% - .mul(99) - .div(100); + // try swapping wS into the pool + const tx = swapXAMOStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("0.0001")); - await assertRemoveOnlyAssets(lpAmount, fixture); + await expect(tx).to.be.revertedWith("Assets balance worse"); }); }); - describe.skip("with a little more wS in the pool", () => { + describe("with a little more wS in the pool", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 20000, depositToStrategy: true, - poolAddEthAmount: 8000, balancePool: true, + poolAddwSAmount: 200, }); beforeEach(async () => { fixture = await loadFixture(); }); - it("Strategist should fail to add too much OS to the pool", async () => { + it("Strategist should add a little OS to the pool", async () => { + const osAmount = parseUnits("8"); + await assertSwapOTokensToPool(osAmount, fixture); + }); + it("Strategist should get the pool close to balanced", async () => { + // just under half the extra wS amount + const osAmount = parseUnits("99"); + await assertSwapOTokensToPool(osAmount, fixture); + }); + it("Strategist should fail to add zero OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; - // Add OS to the pool - const tx = swapXAMOStrategy - .connect(strategist) - .mintAndAddOTokens(parseUnits("10000")); + const tx = swapXAMOStrategy.connect(strategist).swapOTokensToPool(0); - await expect(tx).to.be.revertedWith("Assets overshot peg"); + await expect(tx).to.be.revertedWith("Must swap some OS"); }); - it("Strategist should fail to remove too much ETH from the pool", async () => { + it("Strategist should fail to add too much OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; - // Remove ETH from the pool + // Add OS to the pool const tx = swapXAMOStrategy .connect(strategist) - .removeOnlyAssets(parseUnits("8000")); + .swapOTokensToPool(parseUnits("110")); - await expect(tx).to.be.revertedWith("Assets overshot peg"); + await expect(tx).to.be.revertedWith("OTokens overshot peg"); }); - it("Strategist should fail to remove the little OS from the pool", async () => { + it("Strategist should fail to add more wS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; - // Remove ETH from the pool + // try swapping wS into the pool const tx = swapXAMOStrategy .connect(strategist) - .removeAndBurnOTokens(parseUnits("1")); + .swapAssetsToPool(parseUnits("0.0001")); await expect(tx).to.be.revertedWith("Assets balance worse"); }); }); - - describe.skip("with a little more OS in the pool", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 20000, - depositToStrategy: false, - poolAddOethAmount: 5000, - balancePool: true, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Strategist should fail to remove too much OS from the pool", async () => { - const { swapXGauge, swapXAMOStrategy, strategist } = fixture; - const lpBalance = await swapXGauge.balanceOf(swapXAMOStrategy.address); - const lpAmount = lpBalance - // reduce by 1% - .mul(99) - .div(100); - - // Remove OS from the pool - const tx = swapXAMOStrategy - .connect(strategist) - .removeAndBurnOTokens(lpAmount); - - await expect(tx).to.be.revertedWith("OTokens overshot peg"); - }); - }); }); const snapData = async (fixture) => { @@ -519,8 +506,9 @@ const assertChangedData = async (dataBefore, delta, fixture) => { await swapXAMOStrategy.checkBalance(wS.address), "Strategy's check balance" ).to.withinRange(expectedStratBalance.sub(1), expectedStratBalance); + const expectedSupply = dataBefore.osSupply.add(delta.osSupply); expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( - dataBefore.osSupply.add(delta.osSupply) + expectedSupply ); // Check the pool's reserves @@ -573,133 +561,21 @@ async function assertSwapAssetsToPool(wsAmount, fixture) { // Check emitted event await expect(tx) .to.emit(swapXAMOStrategy, "SwapAssetsToPool") - .withNamedArgs({ _wsAmount: wsAmount }); -} - -async function assertMintAndAddOTokens(oethMintAmount, fixture) { - const { swapXAMOStrategy, swapXPool, oSonic, strategist } = fixture; - - const curveBalancesBefore = await swapXPool.get_balances(); - const osSupplyBefore = await oSonic.totalSupply(); - - log(`Before mint and add ${formatUnits(oethMintAmount)} OS to the pool`); - await run("amoStrat", { - pool: "OS", - output: false, - }); - - // Mint and add OS to the pool - const tx = await swapXAMOStrategy - .connect(strategist) - .mintAndAddOTokens(oethMintAmount); - - const receipt = await tx.wait(); - - // Check emitted event - await expect(tx) - .emit(swapXAMOStrategy, "Deposit") - .withArgs(oSonic.address, swapXPool.address, oethMintAmount); - - log("After mint and add of OS to the pool"); - await run("amoStrat", { - pool: "OS", - output: false, - fromBlock: receipt.blockNumber - 1, - }); - - // Check the ETH and OS balances in the Curve pool - const curveBalancesAfter = await swapXPool.get_balances(); - expect(curveBalancesAfter[0]).to.approxEqualTolerance( - curveBalancesBefore[0], - 0.01 // 0.01% or 1 basis point - ); - expect(curveBalancesAfter[1]).to.approxEqualTolerance( - curveBalancesBefore[1].add(oethMintAmount), - 0.01 // 0.01% - ); - - // Check the OS total supply decrease - expect(await oSonic.totalSupply()).to.approxEqualTolerance( - osSupplyBefore.add(oethMintAmount), - 0.01 // 0.01% or 1 basis point - ); + .withNamedArgs({ wsSwapped: wsAmount }); } -async function assertRemoveOnlyAssets(lpAmount, fixture) { - const { - swapXAMOStrategy, - swapXGauge, - swapXPool, - oSonicVault, - oSonic, - strategist, - wS, - } = fixture; - - log(`Removing ${formatUnits(lpAmount)} ETH from the pool`); - const ethRemoveAmount = await calcWSRemoveAmount(fixture, lpAmount); - log("After calc ETH remove amount"); - const curveBalancesBefore = await swapXPool.get_balances(); - const osSupplyBefore = await oSonic.totalSupply(); - const vaultWethBalanceBefore = await wS.balanceOf(oSonicVault.address); - const strategyLpBalanceBefore = await swapXGauge.balanceOf( - swapXAMOStrategy.address - ); - const vaultValueBefore = await oSonicVault.totalValue(); - - log(`Before remove and burn of ${formatUnits(lpAmount)} ETH from the pool`); - await run("amoStrat", { - pool: "OS", - output: false, - }); +async function assertSwapOTokensToPool(osAmount, fixture) { + const { swapXAMOStrategy, strategist } = fixture; - // Remove ETH from the pool and transfer to the Vault as WETH + // Mint OS and swap into the pool, then mint more OS to add with the wS swapped out const tx = await swapXAMOStrategy .connect(strategist) - .removeOnlyAssets(lpAmount); - - const receipt = await tx.wait(); - - log("After remove and burn of ETH from pool"); - await run("amoStrat", { - pool: "OS", - output: false, - fromBlock: receipt.blockNumber - 1, - }); + .swapOTokensToPool(osAmount); // Check emitted event await expect(tx) - .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(wS.address, swapXPool.address, ethRemoveAmount); - - // Check the ETH and OS balances in the Curve pool - const curveBalancesAfter = await swapXPool.get_balances(); - expect(curveBalancesAfter[0]).to.approxEqualTolerance( - curveBalancesBefore[0].sub(ethRemoveAmount), - 0.01 // 0.01% or 1 basis point - ); - expect(curveBalancesAfter[1]).to.equal(curveBalancesBefore[1]); - - // Check the OS total supply is the same - expect(await oSonic.totalSupply()).to.approxEqualTolerance( - osSupplyBefore, - 0.01 // 0.01% or 1 basis point - ); - - // Check the WETH balance in the Vault - expect(await wS.balanceOf(oSonicVault.address)).to.equal( - vaultWethBalanceBefore.add(ethRemoveAmount) - ); - - // Check the vault made money - const vaultValueAfter = await oSonicVault.totalValue(); - expect(vaultValueAfter.sub(vaultValueBefore)).to.gt(parseUnits("-1")); - - // Check the strategy LP balance decreased - const strategyLpBalanceAfter = await swapXGauge.balanceOf( - swapXAMOStrategy.address - ); - expect(strategyLpBalanceBefore.sub(strategyLpBalanceAfter)).to.eq(lpAmount); + .emit(swapXAMOStrategy, "SwapOTokensToPool") + .withNamedArgs({ osMinted: osAmount }); } // Calculate the minted OS amount for a deposit @@ -755,15 +631,3 @@ async function calcWithdrawAllAmounts(fixture) { osBurnAmount, }; } - -// Calculate the amount of wS burned from a removeOnlyAssets -async function calcWSRemoveAmount(fixture, lpAmount) { - const { swapXPool } = fixture; - - // Get the ETH removed from the pool for a given amount of LP tokens - const ethRemoveAmount = await swapXPool.calc_withdraw_one_coin(lpAmount, 0); - - log(`ETH burn amount : ${formatUnits(ethRemoveAmount)}`); - - return ethRemoveAmount; -} From 5c6bb89027f2dcad43712b38c7c89065ab71d854 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 3 Mar 2025 21:24:08 +1100 Subject: [PATCH 46/80] More SwapX AMO fork tests --- .../sonic/swapx-amo.sonic.fork-test.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index e5ff684828..d3c9644912 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -407,9 +407,24 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await assertSwapOTokensToPool(osAmount, fixture); }); it("Strategist should add a lot of OS to the pool", async () => { - const osAmount = parseUnits("9000"); + const osAmount = parseUnits("5000"); await assertSwapOTokensToPool(osAmount, fixture); }); + it("Strategist should get the pool close to balanced", async () => { + // just under half the extra wS amount + const osAmount = parseUnits("9300"); + await assertSwapOTokensToPool(osAmount, fixture); + }); + it("Strategist should fail to add so much OS that is overshoots", async () => { + const { swapXAMOStrategy, strategist } = fixture; + + // try swapping wS into the pool + const tx = swapXAMOStrategy + .connect(strategist) + .swapOTokensToPool(parseUnits("9990")); + + await expect(tx).to.be.revertedWith("OTokens overshot peg"); + }); it("Strategist should fail to add more wS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; From cb895568070f69e95c8366e1b88feffe945a9057 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 3 Mar 2025 22:34:18 +1100 Subject: [PATCH 47/80] More SwapX fork testing --- .../sonic/swapx-amo.sonic.fork-test.js | 120 +++++++++++------- 1 file changed, 74 insertions(+), 46 deletions(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index d3c9644912..c1654ca8b5 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -516,67 +516,95 @@ const assertChangedData = async (dataBefore, delta, fixture) => { const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = fixture; - const expectedStratBalance = dataBefore.stratBalance.add(delta.stratBalance); - expect( - await swapXAMOStrategy.checkBalance(wS.address), - "Strategy's check balance" - ).to.withinRange(expectedStratBalance.sub(1), expectedStratBalance); - const expectedSupply = dataBefore.osSupply.add(delta.osSupply); - expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( - expectedSupply - ); + if (delta.stratBalance != undefined) { + const expectedStratBalance = dataBefore.stratBalance.add( + delta.stratBalance + ); + expect( + await swapXAMOStrategy.checkBalance(wS.address), + "Strategy's check balance" + ).to.withinRange(expectedStratBalance.sub(1), expectedStratBalance); + } + + if (delta.osSupply != undefined) { + const expectedSupply = dataBefore.osSupply.add(delta.osSupply); + expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( + expectedSupply + ); + } // Check the pool's reserves - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - expect(wsReserves, "wS reserves").to.equal( - dataBefore.reserves.ws.add(delta.reserves.ws) - ); - expect(osReserves, "OS reserves").to.equal( - dataBefore.reserves.os.add(delta.reserves.os) - ); - - // Check the strategy's gauge balance - // Calculate the liquidity added to the pool - const wsLiquidity = delta.reserves.ws - .mul(dataBefore.poolSupply) - .div(dataBefore.reserves.ws); - const osLiquidity = delta.reserves.os - .mul(dataBefore.poolSupply) - .div(dataBefore.reserves.os); - const deltaStratGaugeBalance = wsLiquidity.lt(osLiquidity) - ? wsLiquidity - : osLiquidity; - const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( - deltaStratGaugeBalance - ); - expect( - await swapXGauge.balanceOf(swapXAMOStrategy.address), - "Strategy's gauge balance" - ).to.withinRange( - expectedStratGaugeBalance.sub(1), - expectedStratGaugeBalance.add(1) - ); + if (delta.reserves != undefined) { + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + expect(wsReserves, "wS reserves").to.equal( + dataBefore.reserves.ws.add(delta.reserves.ws) + ); + expect(osReserves, "OS reserves").to.equal( + dataBefore.reserves.os.add(delta.reserves.os) + ); + + // Check the strategy's gauge balance + // Calculate the liquidity added to the pool + const wsLiquidity = delta.reserves.ws + .mul(dataBefore.poolSupply) + .div(dataBefore.reserves.ws); + const osLiquidity = delta.reserves.os + .mul(dataBefore.poolSupply) + .div(dataBefore.reserves.os); + const deltaStratGaugeBalance = wsLiquidity.lt(osLiquidity) + ? wsLiquidity + : osLiquidity; + const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( + deltaStratGaugeBalance + ); + expect( + await swapXGauge.balanceOf(swapXAMOStrategy.address), + "Strategy's gauge balance" + ).to.withinRange( + expectedStratGaugeBalance.sub(1), + expectedStratGaugeBalance.add(1) + ); + } // Check Vault's wS balance - expect( - await wS.balanceOf(oSonicVault.address), - "Vault's wS balance" - ).to.equal(dataBefore.vaultWSBalance.add(delta.vaultWSBalance)); + if (delta.vaultWSBalance != undefined) { + expect( + await wS.balanceOf(oSonicVault.address), + "Vault's wS balance" + ).to.equal(dataBefore.vaultWSBalance.add(delta.vaultWSBalance)); + } }; async function assertSwapAssetsToPool(wsAmount, fixture) { const { swapXAMOStrategy, strategist } = fixture; + const dataBefore = await snapData(fixture); + // Swap wS to the pool and burn the received OS from the pool const tx = await swapXAMOStrategy .connect(strategist) .swapAssetsToPool(wsAmount); // Check emitted event - await expect(tx) - .to.emit(swapXAMOStrategy, "SwapAssetsToPool") - .withNamedArgs({ wsSwapped: wsAmount }); + await expect(tx).to.emittedEvent("SwapAssetsToPool", [ + wsAmount, + (lpTokens) => { + expect(lpTokens).to.approxEqualTolerance(wsAmount, 5); + }, + (osBurnt) => { + // TODO narrow down the range + expect(osBurnt).to.gt(wsAmount, 1); + }, + ]); + + await assertChangedData( + dataBefore, + { + vaultWSBalance: 0, + }, + fixture + ); } async function assertSwapOTokensToPool(osAmount, fixture) { From d84eab476f29eafe8f49bedf9ffd4e0a98da8cea Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 4 Mar 2025 14:11:33 +1100 Subject: [PATCH 48/80] Gov actions to change OS Vault config --- contracts/deploy/sonic/012_vault_config.js | 35 +++++++++++++++++++ contracts/test/_fixture-sonic.js | 2 +- contracts/test/vault/vault.sonic.fork-test.js | 10 +++--- 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 contracts/deploy/sonic/012_vault_config.js diff --git a/contracts/deploy/sonic/012_vault_config.js b/contracts/deploy/sonic/012_vault_config.js new file mode 100644 index 0000000000..69dcb542ff --- /dev/null +++ b/contracts/deploy/sonic/012_vault_config.js @@ -0,0 +1,35 @@ +const { deployOnSonic } = require("../../utils/deploy-l2"); +const { oethUnits } = require("../../test/helpers"); +const { resolveContract } = require("../../utils/resolvers"); + +module.exports = deployOnSonic( + { + deployName: "012_vault_config", + }, + async () => { + const cOSonicVault = await resolveContract("OSonicVaultProxy", "IVault"); + + return { + actions: [ + { + // Increase the rebase threshold to 50k wS + contract: cOSonicVault, + signature: "setRebaseThreshold(uint256)", + args: [oethUnits("20000", 18)], + }, + { + // Increase the auto-allocation threshold to 20k wS + contract: cOSonicVault, + signature: "setAutoAllocateThreshold(uint256)", + args: [oethUnits("20000", 18)], + }, + { + // Reduce the vault buffer to 0.25% + contract: cOSonicVault, + signature: "setVaultBuffer(uint256)", + args: [oethUnits("0.0025", 18)], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index de13ca5d25..76eaea373f 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -153,7 +153,7 @@ const defaultSonicFixture = deployments.createFixture(async () => { await wS.connect(user).deposit({ value: oethUnits("10000000") }); // Set allowance on the vault - await wS.connect(user).approve(oSonicVault.address, oethUnits("5000")); + await wS.connect(user).approve(oSonicVault.address, oethUnits("50000")); } return { diff --git a/contracts/test/vault/vault.sonic.fork-test.js b/contracts/test/vault/vault.sonic.fork-test.js index 3ef6183d51..a136bba8b6 100644 --- a/contracts/test/vault/vault.sonic.fork-test.js +++ b/contracts/test/vault/vault.sonic.fork-test.js @@ -120,7 +120,7 @@ describe("ForkTest: Sonic Vault", function () { // Clear any wS out of the Vault first await oSonicVault.allocate(); - const mintAmount = parseUnits("1000"); + const mintAmount = parseUnits("20000"); const tx = await oSonicVault .connect(nick) .mint(wS.address, mintAmount, 0); @@ -130,7 +130,7 @@ describe("ForkTest: Sonic Vault", function () { .to.emit(oSonicVault, "Mint") .withArgs(nick.address, mintAmount); - // check 99% is deposited to staking strategy + // check 99.75% is deposited to staking strategy await expect(tx).to.emit(sonicStakingStrategy, "Deposit"); }); @@ -222,9 +222,11 @@ describe("ForkTest: Sonic Vault", function () { .withArgs(wS.address, addresses.zero, withdrawAmount); }); - it("Should have vault buffer set to 1%", async () => { + it("Should have vault buffer set", async () => { const { oSonicVault } = fixture; - expect(await oSonicVault.vaultBuffer()).to.equal(parseUnits("1", 16)); + expect(await oSonicVault.vaultBuffer()).to.equal( + parseUnits("0.0025", 18) // 0.25% + ); }); }); }); From 75c0251133785b1de9ffe507401239370b7ec1a6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 4 Mar 2025 15:30:38 +1100 Subject: [PATCH 49/80] Changed the Sonic Vault buffer to 0.5% --- contracts/deploy/sonic/012_vault_config.js | 4 ++-- contracts/test/vault/vault.sonic.fork-test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/deploy/sonic/012_vault_config.js b/contracts/deploy/sonic/012_vault_config.js index 69dcb542ff..67c58c12ab 100644 --- a/contracts/deploy/sonic/012_vault_config.js +++ b/contracts/deploy/sonic/012_vault_config.js @@ -24,10 +24,10 @@ module.exports = deployOnSonic( args: [oethUnits("20000", 18)], }, { - // Reduce the vault buffer to 0.25% + // Reduce the vault buffer to 0.5% contract: cOSonicVault, signature: "setVaultBuffer(uint256)", - args: [oethUnits("0.0025", 18)], + args: [oethUnits("0.005", 18)], }, ], }; diff --git a/contracts/test/vault/vault.sonic.fork-test.js b/contracts/test/vault/vault.sonic.fork-test.js index a136bba8b6..de42c72861 100644 --- a/contracts/test/vault/vault.sonic.fork-test.js +++ b/contracts/test/vault/vault.sonic.fork-test.js @@ -130,7 +130,7 @@ describe("ForkTest: Sonic Vault", function () { .to.emit(oSonicVault, "Mint") .withArgs(nick.address, mintAmount); - // check 99.75% is deposited to staking strategy + // check 99.5% is deposited to staking strategy await expect(tx).to.emit(sonicStakingStrategy, "Deposit"); }); @@ -225,7 +225,7 @@ describe("ForkTest: Sonic Vault", function () { it("Should have vault buffer set", async () => { const { oSonicVault } = fixture; expect(await oSonicVault.vaultBuffer()).to.equal( - parseUnits("0.0025", 18) // 0.25% + parseUnits("0.005", 18) // 0.5% ); }); }); From 4ef572d4696b9f6eb11ac83ebcbdcb60eb51ba82 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 4 Mar 2025 16:03:08 +1100 Subject: [PATCH 50/80] Fix Sonic unit tests --- contracts/test/_fixture-sonic.js | 36 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 76eaea373f..2f9f78d162 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -17,10 +17,6 @@ const BURNER_ROLE = let snapshotId; const defaultSonicFixture = deployments.createFixture(async () => { - // Impersonate governor - const governor = await impersonateAndFund(addresses.sonic.timelock); - governor.address = addresses.sonic.timelock; - if (!snapshotId && !isFork) { snapshotId = await nodeSnapshot(); } @@ -30,14 +26,27 @@ const defaultSonicFixture = deployments.createFixture(async () => { return; } - let deployerAddr; + const { deployerAddr, strategistAddr, timelockAddr, governorAddr } = + await getNamedAccounts(); + if (isFork) { // Fund deployer account - const namedAccounts = await getNamedAccounts(); - deployerAddr = namedAccounts.deployerAddr; await impersonateAndFund(deployerAddr); } + // Impersonate governor + const governorAddress = isFork ? addresses.sonic.timelock : governorAddr; + const governor = await impersonateAndFund(governorAddress); + governor.address = governorAddress; + + // Impersonate strategist + const strategist = await impersonateAndFund(strategistAddr); + strategist.address = strategistAddr; + + // Impersonate strategist + const timelock = await impersonateAndFund(timelockAddr); + timelock.address = timelockAddr; + log( `Before deployments with param "${ isFork ? ["sonic"] : ["sonic_unit_tests"] @@ -65,6 +74,8 @@ const defaultSonicFixture = deployments.createFixture(async () => { oSonicVaultProxy.address ); + const oSonicVaultSigner = await impersonateAndFund(oSonicVault.address); + // Sonic staking strategy const sonicStakingStrategyProxy = await ethers.getContract( "SonicStakingStrategyProxy" @@ -125,17 +136,6 @@ const defaultSonicFixture = deployments.createFixture(async () => { const signers = await hre.ethers.getSigners(); const [minter, burner, rafael, nick, clement] = signers.slice(4); // Skip first 4 addresses to avoid conflict - const { strategistAddr, timelockAddr } = await getNamedAccounts(); - - // Impersonate strategist - const strategist = await impersonateAndFund(strategistAddr); - strategist.address = strategistAddr; - - // Impersonate strategist - const timelock = await impersonateAndFund(timelockAddr); - timelock.address = timelockAddr; - - const oSonicVaultSigner = await impersonateAndFund(oSonicVault.address); let validatorRegistrator; if (isFork) { From 555f22a7ade13e6fe93e4804136608543de107f7 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 4 Mar 2025 16:03:33 +1100 Subject: [PATCH 51/80] Prettier --- contracts/tasks/storageSlots.js | 4 ++-- contracts/utils/addresses.js | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/tasks/storageSlots.js b/contracts/tasks/storageSlots.js index 2e0943651a..844dfb0511 100644 --- a/contracts/tasks/storageSlots.js +++ b/contracts/tasks/storageSlots.js @@ -66,11 +66,11 @@ const loadPreviousStorageLayoutForContract = async (hre, contractName) => { return JSON.parse(await promises.readFile(location, "utf8")); }; -// @dev contractName and contract can be different when the deploy procedure wants to +// @dev contractName and contract can be different when the deploy procedure wants to // store a certain contract deployment under a different name as is the name of // the contract in the source code. // @param contract the name of the contract as is in the source code of the contract -// @param contractName a potential override of the contract as is to be stored in the +// @param contractName a potential override of the contract as is to be stored in the // deployment descriptors const storeStorageLayoutForContract = async (hre, contractName, contract) => { const layout = await getStorageLayoutForContract(hre, contractName, contract); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 48c1694c4b..3aaa4b899a 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -409,14 +409,18 @@ addresses.sonic.SwapXOsGEMSxMultisigBooster = addresses.sonic.SwapX = {}; addresses.sonic.SwapX.OsHedgy = {}; -addresses.sonic.SwapX.OsHedgy.pool = "0x1695d6bd8d8adc8b87c6204be34d34d19a3fe1d6"; -addresses.sonic.SwapX.OsHedgy.yf_treasury = "0x4c884677427a975d1b99286e99188c82d71223c8"; +addresses.sonic.SwapX.OsHedgy.pool = + "0x1695d6bd8d8adc8b87c6204be34d34d19a3fe1d6"; +addresses.sonic.SwapX.OsHedgy.yf_treasury = + "0x4c884677427a975d1b99286e99188c82d71223c8"; // Sonic Shadow addresses.sonic.Shadow = {}; addresses.sonic.Shadow.OsEco = {}; -addresses.sonic.Shadow.OsEco.pool = "0xfd0cee796348fd99ab792c471f4419b4c56cf6b8"; -addresses.sonic.Shadow.OsEco.yf_treasury = "0x4b9919603170c77936d8ec2c08b604844e861699"; +addresses.sonic.Shadow.OsEco.pool = + "0xfd0cee796348fd99ab792c471f4419b4c56cf6b8"; +addresses.sonic.Shadow.OsEco.yf_treasury = + "0x4b9919603170c77936d8ec2c08b604844e861699"; // Sonic Curve addresses.sonic.WS_OS = {}; From 0b322f0f0ca4a38dbe86558067e8d15b36522bb3 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 4 Mar 2025 16:20:52 +1100 Subject: [PATCH 52/80] Ran Sonic 012 deployment --- contracts/deployments/sonic/.migrations.json | 3 +- .../operations/012_vault_config.execute.json | 52 +++++++++++++++++ .../operations/012_vault_config.schedule.json | 57 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 contracts/deployments/sonic/operations/012_vault_config.execute.json create mode 100644 contracts/deployments/sonic/operations/012_vault_config.schedule.json diff --git a/contracts/deployments/sonic/.migrations.json b/contracts/deployments/sonic/.migrations.json index 16bf3a0def..f2df446624 100644 --- a/contracts/deployments/sonic/.migrations.json +++ b/contracts/deployments/sonic/.migrations.json @@ -7,5 +7,6 @@ "006_yf_swpx_os_pool": 1738198367, "008_swapx_yield_forward": 1738873151, "010_swapx_yield_forward": 1740000624, - "011_pool_booster_factory": 1740172411 + "011_pool_booster_factory": 1740172411, + "012_vault_config": 1741065376 } \ No newline at end of file diff --git a/contracts/deployments/sonic/operations/012_vault_config.execute.json b/contracts/deployments/sonic/operations/012_vault_config.execute.json new file mode 100644 index 0000000000..73417417d5 --- /dev/null +++ b/contracts/deployments/sonic/operations/012_vault_config.execute.json @@ -0,0 +1,52 @@ +{ + "version": "1.0", + "chainId": "146", + "createdAt": 1741065376, + "meta": { + "name": "Transaction Batch", + "description": "", + "txBuilderVersion": "1.16.1", + "createdFromSafeAddress": "0xAdDEA7933Db7d83855786EB43a238111C69B00b6", + "createdFromOwnerAddress": "" + }, + "transactions": [ + { + "to": "0x31a91336414d3B955E494E7d485a6B06b55FC8fB", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "type": "address[]", + "name": "targets" + }, + { + "type": "uint256[]", + "name": "values" + }, + { + "type": "bytes[]", + "name": "payloads" + }, + { + "type": "bytes32", + "name": "predecessor" + }, + { + "type": "bytes32", + "name": "salt" + } + ], + "name": "executeBatch", + "payable": true + }, + "contractInputsValues": { + "targets": "[\"0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186\",\"0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186\",\"0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186\"]", + "values": "[\"0\",\"0\",\"0\"]", + "payloads": "[\"0xb890ebf600000000000000000000000000000000000000000000043c33c1937564800000\",\"0xb2c9336d00000000000000000000000000000000000000000000043c33c1937564800000\",\"0x8ec489a20000000000000000000000000000000000000000000000000011c37937e08000\"]", + "predecessor": "0x0000000000000000000000000000000000000000000000000000000000000000", + "salt": "0x32e382b0c71d7b38e8aede64b9736790610f979a1cc21436a82d3477a393ef2c" + } + } + ] +} \ No newline at end of file diff --git a/contracts/deployments/sonic/operations/012_vault_config.schedule.json b/contracts/deployments/sonic/operations/012_vault_config.schedule.json new file mode 100644 index 0000000000..b717762f7a --- /dev/null +++ b/contracts/deployments/sonic/operations/012_vault_config.schedule.json @@ -0,0 +1,57 @@ +{ + "version": "1.0", + "chainId": "146", + "createdAt": 1741065375, + "meta": { + "name": "Transaction Batch", + "description": "", + "txBuilderVersion": "1.16.1", + "createdFromSafeAddress": "0xAdDEA7933Db7d83855786EB43a238111C69B00b6", + "createdFromOwnerAddress": "" + }, + "transactions": [ + { + "to": "0x31a91336414d3B955E494E7d485a6B06b55FC8fB", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [ + { + "type": "address[]", + "name": "targets" + }, + { + "type": "uint256[]", + "name": "values" + }, + { + "type": "bytes[]", + "name": "payloads" + }, + { + "type": "bytes32", + "name": "predecessor" + }, + { + "type": "bytes32", + "name": "salt" + }, + { + "type": "uint256", + "name": "delay" + } + ], + "name": "scheduleBatch", + "payable": false + }, + "contractInputsValues": { + "targets": "[\"0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186\",\"0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186\",\"0xa3c0eCA00D2B76b4d1F170b0AB3FdeA16C180186\"]", + "values": "[\"0\",\"0\",\"0\"]", + "payloads": "[\"0xb890ebf600000000000000000000000000000000000000000000043c33c1937564800000\",\"0xb2c9336d00000000000000000000000000000000000000000000043c33c1937564800000\",\"0x8ec489a20000000000000000000000000000000000000000000000000011c37937e08000\"]", + "predecessor": "0x0000000000000000000000000000000000000000000000000000000000000000", + "salt": "0x32e382b0c71d7b38e8aede64b9736790610f979a1cc21436a82d3477a393ef2c", + "delay": "86400" + } + } + ] +} \ No newline at end of file From 6bbafeb0f5dc2a0cbb7a8b696e36ca6601387054 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 4 Mar 2025 17:23:29 +1100 Subject: [PATCH 53/80] Prettier --- contracts/deploy/sonic/012_tb_yf_batch_1.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/sonic/012_tb_yf_batch_1.js b/contracts/deploy/sonic/012_tb_yf_batch_1.js index 18006989a8..397d0db3ab 100644 --- a/contracts/deploy/sonic/012_tb_yf_batch_1.js +++ b/contracts/deploy/sonic/012_tb_yf_batch_1.js @@ -47,7 +47,7 @@ module.exports = deployOnSonic( addresses.sonic.OSonicProxy, addresses.sonic.timelock, cPoolBoostCentralRegistryProxy.address, - ], + ] ); const cPoolBoosterFactorySwapxSingle = await ethers.getContract( "PoolBoosterFactorySwapxSingle" From 139c6c17ca6548621c70fc27661efaf2d8bd0027 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 4 Mar 2025 17:39:33 +1100 Subject: [PATCH 54/80] Fixed schedule Timelock --- ..._vault_config.schedule.json => 013_vault_config.schedule.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/deployments/sonic/operations/{012_vault_config.schedule.json => 013_vault_config.schedule.json} (100%) diff --git a/contracts/deployments/sonic/operations/012_vault_config.schedule.json b/contracts/deployments/sonic/operations/013_vault_config.schedule.json similarity index 100% rename from contracts/deployments/sonic/operations/012_vault_config.schedule.json rename to contracts/deployments/sonic/operations/013_vault_config.schedule.json From 38415601b7701c994eed6db8c67cbc04f1667091 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 4 Mar 2025 21:48:36 +1100 Subject: [PATCH 55/80] Fix fork tests --- contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index c1654ca8b5..f3f594cc84 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -313,7 +313,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await assertSwapAssetsToPool(parseUnits("3000"), fixture); }); it("Strategist should swap most of the wS owned by the strategy", async () => { - await assertSwapAssetsToPool(parseUnits("4500"), fixture); + // TODO calculate how much wS should be swapped to get the pool balanced + await assertSwapAssetsToPool(parseUnits("4400"), fixture); }); it("Strategist should fail to swap all wS owned by the strategy", async () => { const { swapXAMOStrategy, strategist } = fixture; @@ -412,7 +413,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); it("Strategist should get the pool close to balanced", async () => { // just under half the extra wS amount - const osAmount = parseUnits("9300"); + const osAmount = parseUnits("9000"); await assertSwapOTokensToPool(osAmount, fixture); }); it("Strategist should fail to add so much OS that is overshoots", async () => { From bbf828156d673c526d55d8629f3a5437026b4897 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 5 Mar 2025 10:24:22 +1100 Subject: [PATCH 56/80] Updated deploy script number --- contracts/deploy/sonic/{011_swapx_amo.js => 014_swapx_amo.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contracts/deploy/sonic/{011_swapx_amo.js => 014_swapx_amo.js} (98%) diff --git a/contracts/deploy/sonic/011_swapx_amo.js b/contracts/deploy/sonic/014_swapx_amo.js similarity index 98% rename from contracts/deploy/sonic/011_swapx_amo.js rename to contracts/deploy/sonic/014_swapx_amo.js index 9441066619..c0bc556eec 100644 --- a/contracts/deploy/sonic/011_swapx_amo.js +++ b/contracts/deploy/sonic/014_swapx_amo.js @@ -7,7 +7,7 @@ const addresses = require("../../utils/addresses"); module.exports = deployOnSonic( { - deployName: "011_swapx_amo", + deployName: "014_swapx_amo", }, async ({ ethers }) => { const { deployerAddr } = await getNamedAccounts(); From c674f1ca1019045fad5d55fefa2f2ec9e074b11f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 5 Mar 2025 10:25:30 +1100 Subject: [PATCH 57/80] Check pool and gauge in the constructor --- .../contracts/interfaces/sonic/ISwapXGauge.sol | 2 ++ .../strategies/sonic/SonicSwapXAMOStrategy.sol | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/contracts/contracts/interfaces/sonic/ISwapXGauge.sol b/contracts/contracts/interfaces/sonic/ISwapXGauge.sol index d363ae116b..9e5def5e5a 100644 --- a/contracts/contracts/interfaces/sonic/ISwapXGauge.sol +++ b/contracts/contracts/interfaces/sonic/ISwapXGauge.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; interface IGauge { + function TOKEN() external view returns (address); + function balanceOf(address account) external view returns (uint256); function claimFees() external returns (uint256 claimed0, uint256 claimed1); diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 0bf1639185..daf6c92c49 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -117,9 +117,24 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { os = _os; ws = _ws; + // Check the pool tokens are correct + require( + IPair(_baseConfig.platformAddress).token0() == _ws && + IPair(_baseConfig.platformAddress).token1() == _os, + "Incorrect pool tokens" + ); + + // Check the gauge is for the pool + require( + IGauge(_gauge).TOKEN() == _baseConfig.platformAddress, + "Incorrect gauge" + ); + + // Set the immutable variables pool = _baseConfig.platformAddress; gauge = _gauge; + // This is an implementation contract. The governor is set in the proxy contract. _setGovernor(address(0)); } From a76a73e8cfbc1ccfe90f003f144094e3043bf2ee Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 5 Mar 2025 22:43:40 +1100 Subject: [PATCH 58/80] Added amm param to amoStrat Hardhat task --- contracts/tasks/amoStrategy.js | 2 +- contracts/tasks/tasks.js | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/tasks/amoStrategy.js b/contracts/tasks/amoStrategy.js index d9ca81df81..9a434239a0 100644 --- a/contracts/tasks/amoStrategy.js +++ b/contracts/tasks/amoStrategy.js @@ -18,7 +18,7 @@ const { logTxDetails } = require("../utils/txLogger"); const log = require("../utils/logger")("task:curve"); /** - * hardhat task that dumps the current state of a AMO Strategy + * Hardhat task that dumps the current state of an AMO strategy */ async function amoStrategyTask(taskArguments) { const poolOTokenSymbol = taskArguments.pool; diff --git a/contracts/tasks/tasks.js b/contracts/tasks/tasks.js index 6cd0a3f6ba..0d1bc91b63 100644 --- a/contracts/tasks/tasks.js +++ b/contracts/tasks/tasks.js @@ -700,7 +700,7 @@ task("curvePool").setAction(async (_, __, runSuper) => { }); // Curve Pools -subtask("amoStrat", "Dumps the current state of a AMO strategy") +subtask("amoStrat", "Dumps the current state of an AMO strategy") .addParam("pool", "Symbol of the curve Metapool. OUSD or OETH") .addOptionalParam( "block", @@ -720,6 +720,12 @@ subtask("amoStrat", "Dumps the current state of a AMO strategy") true, types.boolean ) + .addOptionalParam( + "amm", + "Type of pool. eg curve, balancer or swapx", + "curve", + types.string + ) .setAction(amoStrategyTask); task("amoStrat").setAction(async (_, __, runSuper) => { return runSuper(); From 0fe013ddc38fdd9d91cc3dffc5c6a023c393d197 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 5 Mar 2025 23:11:44 +1100 Subject: [PATCH 59/80] New checkBalance implementation for SwapX AMO --- .../sonic/SonicSwapXAMOStrategy.sol | 158 +++++++++++++++++- .../sonic/swapx-amo.sonic.fork-test.js | 102 +++++++++++ 2 files changed, 251 insertions(+), 9 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index daf6c92c49..d2c3ae5a35 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -11,6 +11,7 @@ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { StableMath } from "../../utils/StableMath.sol"; +import { IBasicToken } from "../../interfaces/IBasicToken.sol"; import { IVault } from "../../interfaces/IVault.sol"; import { IPair } from "../../interfaces/sonic/ISwapXPair.sol"; import { IGauge } from "../../interfaces/sonic/ISwapXGauge.sol"; @@ -25,6 +26,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * draining the protocol funds. */ uint256 public constant SOLVENCY_THRESHOLD = 0.998 ether; + /// @dev Precision for the SwapX stable AMM invariant + uint256 public constant PRECISION = 1e18; // New immutable variables that must be set in the constructor /** @@ -114,16 +117,23 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { address _ws, address _gauge ) InitializableAbstractStrategy(_baseConfig) { - os = _os; - ws = _ws; - // Check the pool tokens are correct require( IPair(_baseConfig.platformAddress).token0() == _ws && IPair(_baseConfig.platformAddress).token1() == _os, "Incorrect pool tokens" ); - + // Checked both tokens are to 18 decimals + require( + IBasicToken(_ws).decimals() == 18 && + IBasicToken(_os).decimals() == 18, + "Incorrect token decimals" + ); + // Check the SwapX pool is a Stable AMM (sAMM) + require( + IPair(_baseConfig.platformAddress).isStable() == true, + "Pool not stable" + ); // Check the gauge is for the pool require( IGauge(_gauge).TOKEN() == _baseConfig.platformAddress, @@ -131,6 +141,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { ); // Set the immutable variables + os = _os; + ws = _ws; pool = _baseConfig.platformAddress; gauge = _gauge; @@ -471,6 +483,44 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // via the improvePoolBalance and strategyValueChecker modifiers } + /// @notice Calculate the value of a LP position in a SwapX stable pool + /// if the pool was balanced + function lpValue(uint256 lpTokens) public view returns (uint256) { + // Get total supply of LP tokens + uint256 totalSupply = IPair(pool).totalSupply(); + require(totalSupply > 0, "No liquidity in pool"); + + // Get the current reserves of the pool + (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); + + // Calculate the invariant of the pool assuming both tokens have 18 decimals. + // k is scaled to 18 decimals. + uint256 k = _invariant(wsReserves, osReserves); + + // If x = y, let’s denote x = y = z (where z is the common reserve value) + // Substitute z into the invariant: + // k = z^3 * z + z * z^3 + // k = 2 * z^4 + // Going back the other way to calculate the common reserve value z + // z = (k / 2) ^ (1/4) + // the total value of the pool when x = y is 2 * z, which is 2 * (k / 2) ^ (1/4) + uint256 zSquared = sqrt((k * 1e18) / 2); // 18 + 18 = 36 decimals becomes 18 decimals after sqrt + uint256 z = sqrt(zSquared * 1e18); // 18 + 18 = 36 decimals becomes 18 decimals after sqrt + uint256 totalValueOfPool = 2 * z; + + // lp value = lp tokens * value of pool / total supply + return (lpTokens * totalValueOfPool) / totalSupply; + } + + /// @dev Compute the invariant for a SwapX stable pool. + /// This assumed both x and y tokens are to 18 decimals. + // k = x^3 * y + x * y^3 + function _invariant(uint256 x, uint256 y) public pure returns (uint256) { + uint256 _a = (x * y) / PRECISION; + uint256 _b = ((x * x) / PRECISION + (y * y) / PRECISION); + return (_a * _b) / PRECISION; + } + /** * Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can * keep rebalancing the pool in both directions making the protocol lose a tiny amount of @@ -525,15 +575,12 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // wS balance needed here for the balance check that happens from vault during depositing. balance = IERC20(ws).balanceOf(address(this)); + // This assumes 1 gauge LP token = 1 pool LP token uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); if (lpTokens == 0) return balance; // Add the strategy’s share of the wS and OS tokens in the SwapX pool. - // (pool’s wS reserves + pool’s OS reserves) * strategy’s LP tokens / total supply of pool LP tokens - (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); - balance += - ((wsReserves + osReserves) * lpTokens) / - IPair(pool).totalSupply(); + balance += lpValue(lpTokens); } /** @@ -588,4 +635,97 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { function _max(int256 a, int256 b) internal pure returns (int256) { return a >= b ? a : b; } + + /// @dev including in here for now. This is a copy of PRBMath's sqrt function. + /// Can either move out into a new lib or import from the use the PRBMath project + + /// @notice Calculates the square root of x using the Babylonian method. + /// + /// @dev See https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method. + /// + /// Notes: + /// - If x is not a perfect square, the result is rounded down. + /// - Credits to OpenZeppelin for the explanations in comments below. + /// + /// @param x The uint256 number for which to calculate the square root. + /// @return result The result as a uint256. + /// @custom:smtchecker abstract-function-nondet + function sqrt(uint256 x) public pure returns (uint256 result) { + if (x == 0) { + return 0; + } + + // For our first guess, we calculate the biggest power of 2 which is smaller than the square root of x. + // + // We know that the "msb" (most significant bit) of x is a power of 2 such that we have: + // + // $$ + // msb(x) <= x <= 2*msb(x)$ + // $$ + // + // We write $msb(x)$ as $2^k$, and we get: + // + // $$ + // k = log_2(x) + // $$ + // + // Thus, we can write the initial inequality as: + // + // $$ + // 2^{log_2(x)} <= x <= 2*2^{log_2(x)+1} \\ + // sqrt(2^k) <= sqrt(x) < sqrt(2^{k+1}) \\ + // 2^{k/2} <= sqrt(x) < 2^{(k+1)/2} <= 2^{(k/2)+1} + // $$ + // + // Consequently, $2^{log_2(x) /2} is a good first approximation of sqrt(x) with at least one correct bit. + uint256 xAux = uint256(x); + result = 1; + if (xAux >= 2**128) { + xAux >>= 128; + result <<= 64; + } + if (xAux >= 2**64) { + xAux >>= 64; + result <<= 32; + } + if (xAux >= 2**32) { + xAux >>= 32; + result <<= 16; + } + if (xAux >= 2**16) { + xAux >>= 16; + result <<= 8; + } + if (xAux >= 2**8) { + xAux >>= 8; + result <<= 4; + } + if (xAux >= 2**4) { + xAux >>= 4; + result <<= 2; + } + if (xAux >= 2**2) { + result <<= 1; + } + + // At this point, `result` is an estimation with at least one bit of precision. We know the true value has at + // most 128 bits, since it is the square root of a uint256. Newton's method converges quadratically (precision + // doubles at every iteration). We thus need at most 7 iteration to turn our partial result with one bit of + // precision into the expected uint128 result. + unchecked { + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + + // If x is not a perfect square, round the result toward zero. + uint256 roundedResult = x / result; + if (result >= roundedResult) { + result = roundedResult; + } + } + } } diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index f3f594cc84..bc21ff2500 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -5,6 +5,7 @@ const { createFixtureLoader } = require("../../_fixture"); const { swapXAMOFixture } = require("../../_fixture-sonic"); const { isCI } = require("../../helpers"); const addresses = require("../../../utils/addresses"); +const { ethers } = require("hardhat"); const log = require("../../../utils/logger")("test:fork:sonic:swapx:amo"); @@ -96,6 +97,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture; const dataBefore = await snapData(fixture); + await logSnapData(dataBefore); const wsDepositAmount = await parseUnits("5000"); const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); @@ -180,6 +182,91 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); + function sqrt(value) { + const ONE = ethers.BigNumber.from(1); + const TWO = ethers.BigNumber.from(2); + + const x = ethers.BigNumber.from(value); + let z = x.add(ONE).div(TWO); + let y = x; + while (z.sub(y).isNegative()) { + y = z; + z = x.div(z).add(z).div(TWO); + } + return y; + } + + describe("Pool testing", () => { + const loadFixture = createFixtureLoader(swapXAMOFixture, { + wsMintAmount: 100, + depositToStrategy: true, + balancePool: true, + }); + beforeEach(async () => { + fixture = await loadFixture(); + }); + it("current state", async () => { + const data = await snapData(fixture); + await logSnapData(data); + + const x = parseUnits("500"); + const y = parseUnits("500"); + const precision = parseUnits("1", 18); + const a = x.mul(y).div(precision); + const b = x.mul(x).div(precision).add(y.mul(y).div(precision)); + const k = a.mul(b).div(precision); + log(`local k for 50:50 = ${formatUnits(k)}`); + + const remoteK = await fixture.swapXAMOStrategy._invariant(x, y); + log(`remote k for 50:50 = ${formatUnits(remoteK)}`); + + const remoteKactual = await fixture.swapXAMOStrategy._invariant( + data.reserves.ws, + data.reserves.os + ); + log(`remote k actual = ${formatUnits(remoteKactual)}`); + + log(`local sqrt(2500) = ${sqrt(2500)}`); + const remoteSqrt125 = await fixture.swapXAMOStrategy.sqrt("2500"); + log(`remote sqrt(2500) = ${remoteSqrt125}`); + + const lpValue = await fixture.swapXAMOStrategy.lpValue(parseUnits("250")); + log(`LP value = ${formatUnits(lpValue)}`); + + const cubedPrecision = parseUnits("1", 54); + const z1 = sqrt(sqrt(k.mul(cubedPrecision).div(2))); + log(`first z = ${formatUnits(z1)}`); + + const adjustedK2 = k.mul(parseUnits("1", 36)).div(2); + const sqrt1 = sqrt(adjustedK2); + const z2 = sqrt(sqrt1.mul(parseUnits("1", 9))); + log(`second z = ${formatUnits(z2)}`); + + const adjustedK3 = k.mul(parseUnits("1", 18)).div(2); + const sqrt2 = sqrt(adjustedK3); + const z3 = sqrt(sqrt2.mul(parseUnits("1", 18))); + log(`third z = ${formatUnits(z3)}`); + + const adjustedK4 = k.mul(parseUnits("1", 12)).div(2); + const sqrt3 = sqrt(adjustedK4); + const z4 = sqrt(sqrt3.mul(parseUnits("1", 21))); + log(`fourth z = ${formatUnits(z4)}`); + + // const adjustedK4 = k.mul(parseUnits("1", 12)).div(2); + const sqrt4 = sqrt(k.div(2)); + const z5 = sqrt(sqrt4.mul(parseUnits("1", 9))); + log(`fifth z = ${formatUnits(z5.mul(parseUnits("1", 9)))}`); + + const val = sqrt(sqrt(k.mul(8)).mul(parseUnits("1", 9))); + log(`alt value = ${formatUnits(val)}`); + + const fiftySquared = ethers.BigNumber.from(2500); + const sqrtFiftySquared = sqrt(fiftySquared); + log(`sqrt 2,500 ${sqrtFiftySquared}`); + expect(sqrtFiftySquared).to.equal(50); + }); + }); + describe("with the strategy having some OS and wS in the pool", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, @@ -194,6 +281,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture; const dataBefore = await snapData(fixture); + await logSnapData(dataBefore); const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts( fixture @@ -499,6 +587,7 @@ const snapData = async (fixture) => { const stratGaugeBalance = await swapXGauge.balanceOf( swapXAMOStrategy.address ); + const gaugeSupply = await swapXGauge.totalSupply(); const vaultWSBalance = await wS.balanceOf(oSonicVault.address); const stratWSBalance = await wS.balanceOf(swapXAMOStrategy.address); @@ -508,11 +597,24 @@ const snapData = async (fixture) => { poolSupply, reserves: { ws: wsReserves, os: osReserves }, stratGaugeBalance, + gaugeSupply, vaultWSBalance, stratWSBalance, }; }; +const logSnapData = async (data) => { + log(`Strategy balance : ${formatUnits(data.stratBalance)}`); + log(`OS supply : ${formatUnits(data.osSupply)}`); + log(`pool supply : ${formatUnits(data.poolSupply)}`); + log(`reserves wS : ${formatUnits(data.reserves.ws)}`); + log(`reserves OS : ${formatUnits(data.reserves.os)}`); + log(`strat gauge balance : ${formatUnits(data.stratGaugeBalance)}`); + log(`gauge supply : ${formatUnits(data.gaugeSupply)}`); + log(`vault wS balance : ${formatUnits(data.vaultWSBalance)}`); + log(`strat wS balance : ${formatUnits(data.stratWSBalance)}`); +}; + const assertChangedData = async (dataBefore, delta, fixture) => { const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = fixture; From 485879f87dd908ae41dabf5306088f09840368eb Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 6 Mar 2025 16:43:45 +1100 Subject: [PATCH 60/80] Updated deploy script number --- .../{124_oeth_vault_upgrade.js => 125_oeth_vault_upgrade.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contracts/deploy/mainnet/{124_oeth_vault_upgrade.js => 125_oeth_vault_upgrade.js} (97%) diff --git a/contracts/deploy/mainnet/124_oeth_vault_upgrade.js b/contracts/deploy/mainnet/125_oeth_vault_upgrade.js similarity index 97% rename from contracts/deploy/mainnet/124_oeth_vault_upgrade.js rename to contracts/deploy/mainnet/125_oeth_vault_upgrade.js index 59f9530940..3d345ad9ea 100644 --- a/contracts/deploy/mainnet/124_oeth_vault_upgrade.js +++ b/contracts/deploy/mainnet/125_oeth_vault_upgrade.js @@ -3,7 +3,7 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); module.exports = deploymentWithGovernanceProposal( { - deployName: "124_oeth_vault_upgrade", + deployName: "125_oeth_vault_upgrade", forceDeploy: false, //forceSkip: true, reduceQueueTime: true, From 264589c1eed2ecbc9317a28400975e6d9ee49722 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 6 Mar 2025 18:23:26 +1100 Subject: [PATCH 61/80] Polish SwapX AMO strategy contract --- .../sonic/SonicSwapXAMOStrategy.sol | 224 +++++++++++------- 1 file changed, 136 insertions(+), 88 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index d2c3ae5a35..b34116bd21 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.0; /** * @title SwapX Automated Market Maker (AMO) Strategy - * @notice AMO strategy for the SwapX OS/wS pool + * @notice AMO strategy for the SwapX OS/wS stable pool * @author Origin Protocol Inc */ -import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { StableMath } from "../../utils/StableMath.sol"; @@ -17,16 +17,17 @@ import { IPair } from "../../interfaces/sonic/ISwapXPair.sol"; import { IGauge } from "../../interfaces/sonic/ISwapXGauge.sol"; contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { + using SafeERC20 for IERC20; using StableMath for uint256; using SafeCast for uint256; /** - * @dev a threshold under which the contract no longer allows for the protocol to manually rebalance. + * @notice a threshold under which the contract no longer allows for the protocol to manually rebalance. * Guarding against a strategist / guardian being taken over and with multiple transactions * draining the protocol funds. */ uint256 public constant SOLVENCY_THRESHOLD = 0.998 ether; - /// @dev Precision for the SwapX stable AMM invariant + /// @notice Precision for the SwapX Stable AMM (sAMM) invariant uint256 public constant PRECISION = 1e18; // New immutable variables that must be set in the constructor @@ -63,7 +64,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { ); /** - * @dev Verifies that the caller is the Strategist. + * @dev Verifies that the caller is the Strategist of the Vault. */ modifier onlyStrategist() { require( @@ -73,6 +74,14 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { _; } + /** + * @dev Skim the SwapX pool in case any extra wS or OS tokens were added + */ + modifier skimPool() { + IPair(pool).skim(address(this)); + _; + } + /** * @dev Checks the pool's balances have improved and the balances * have not tipped to the other side. @@ -154,11 +163,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * Initializer for setting up strategy internal state. This overrides the * InitializableAbstractStrategy initializer as SwapX strategies don't fit * well within that abstraction. - * @param _rewardTokenAddresses Address of CRV + * @param _rewardTokenAddresses Address of SWPx token */ - function initialize( - address[] calldata _rewardTokenAddresses // CRV - ) external onlyGovernor initializer { + function initialize(address[] calldata _rewardTokenAddresses) + external + onlyGovernor + initializer + { address[] memory pTokens = new address[](1); pTokens[0] = pool; @@ -180,47 +191,45 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { /** * @notice Deposit Wrapped S (wS) into the SwapX pool - * @param _wS Address of Wrapped S (wS) contract. + * @param _asset Address of Wrapped S (wS) contract. * @param _amount Amount of Wrapped S (wS) to deposit. */ - function deposit(address _wS, uint256 _amount) + function deposit(address _asset, uint256 _amount) external override onlyVault nonReentrant + skimPool { - _deposit(_wS, _amount); + _deposit(_asset, _amount); } function _deposit(address _wS, uint256 _wsAmount) internal { require(_wsAmount > 0, "Must deposit something"); - require(_wS == ws, "Can only deposit wS"); - - emit Deposit(_wS, pool, _wsAmount); + require(_wS == ws, "Unsupported asset"); - // Calculate the required amount of OS to mint based on the wS amount - // This ensure the proportion of OS tokens being added to the pool matches the proportion of wS tokens. - // For example, if the added wS tokens is 10% of existing wS tokens in the pool, - // then the OS tokens being added should also be 10% of the OS tokens in the pool. - (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); - uint256 osAmount = (_wsAmount * osReserves) / wsReserves; + // Calculate the required amount of OS to mint based on the wS amount. + uint256 osLiquidity = _calcTokensToMint(_wsAmount); - // Mint the required OS tokens to the strategy - IVault(vaultAddress).mintForStrategy(osAmount); + // Mint the required OS tokens to this strategy + IVault(vaultAddress).mintForStrategy(osLiquidity); // Add wS and OS liquidity to the pool and stake in gauge - _depositToPoolAndGauge(_wsAmount, osAmount); - - emit Deposit(os, pool, osAmount); + _depositToPoolAndGauge(_wsAmount, osLiquidity); // Ensure solvency of the vault _solvencyAssert(); + + // Emit event for the deposited wS tokens + emit Deposit(_wS, pool, _wsAmount); + // Emit event for the minted OS tokens + emit Deposit(os, pool, osLiquidity); } /** * @notice Deposit the strategy's entire balance of Wrapped S (wS) into the pool */ - function depositAll() external override onlyVault nonReentrant { + function depositAll() external override onlyVault nonReentrant skimPool { uint256 balance = IERC20(ws).balanceOf(address(this)); if (balance > 0) { _deposit(ws, balance); @@ -235,24 +244,19 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * @notice Withdraw wS and OS from the SwapX pool, burn the OS, * and transfer the wS to the recipient. * @param _recipient Address to receive withdrawn asset which is normally the Vault. - * @param _ws Address of the Wrapped S (wS) contract. + * @param _asset Address of the Wrapped S (wS) contract. * @param _wsAmount Amount of Wrapped S (wS) to withdraw. */ function withdraw( address _recipient, - address _ws, + address _asset, uint256 _wsAmount - ) external override onlyVault nonReentrant { + ) external override onlyVault nonReentrant skimPool { require(_wsAmount > 0, "Must withdraw something"); - require(_ws == ws, "Can only withdraw wS"); - - emit Withdrawal(_ws, pool, _wsAmount); - - // Skim the pool in case extra tokens were added - IPair(pool).skim(address(this)); + require(_asset == ws, "Unsupported asset"); // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back - uint256 lpTokens = calcTokensToBurn(_wsAmount); + uint256 lpTokens = _calcTokensToBurn(_wsAmount); // Withdraw pool LP tokens from the gauge and remove assets from from the pool _withdrawFromGaugeAndPool(lpTokens); @@ -261,23 +265,34 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { uint256 osToBurn = IERC20(os).balanceOf(address(this)); IVault(vaultAddress).burnForStrategy(osToBurn); - emit Withdrawal(os, pool, osToBurn); - // Transfer wS to the recipient - require( - IERC20(ws).transfer(_recipient, _wsAmount), - "Transfer of wS not successful" - ); + IERC20(ws).safeTransfer(_recipient, _wsAmount); // Ensure solvency of the vault _solvencyAssert(); + + // Emit event for the withdrawn wS tokens + emit Withdrawal(_asset, pool, _wsAmount); + // Emit event for the burnt OS tokens + emit Withdrawal(os, pool, osToBurn); } /** - * @notice Remove all wS and OS from the SwapX pool, burn all the OS, + * @notice Withdraw all pool LP tokens from the gauge, + * remove all wS and OS from the SwapX pool, + * burn all the OS tokens, * and transfer all the wS to the Vault contract. + * @dev There is no solvency check here as withdrawAll can be called to + * quickly secure assets to the Vault in emergencies. */ - function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + function withdrawAll() + external + override + onlyVaultOrGovernor + nonReentrant + skimPool + { + // Get all the pool LP tokens the strategy has staked in the gauge uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); // Can not withdraw zero LP tokens from the gauge if (lpTokens == 0) return; @@ -285,7 +300,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Withdraw pool LP tokens from the gauge and remove assets from from the pool _withdrawFromGaugeAndPool(lpTokens); - // Burn all OS + // Burn all OS in this strategy contract uint256 osToBurn = IERC20(os).balanceOf(address(this)); IVault(vaultAddress).burnForStrategy(osToBurn); @@ -293,12 +308,11 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // This includes all that was removed from the SwapX pool and // any that was sitting in the strategy contract before the removal. uint256 wsBalance = IERC20(ws).balanceOf(address(this)); - require( - IERC20(ws).transfer(vaultAddress, wsBalance), - "Transfer of wS not successful" - ); + IERC20(ws).safeTransfer(vaultAddress, wsBalance); + // Emit event for the withdrawn wS tokens emit Withdrawal(ws, pool, wsBalance); + // Emit event for the burnt OS tokens emit Withdrawal(os, pool, osToBurn); } @@ -316,16 +330,14 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { onlyStrategist nonReentrant improvePoolBalance + skimPool { require(_wsAmount > 0, "Must swap some wS"); - // Skim the pool in case extra tokens were added - IPair(pool).skim(address(this)); - // 1. Partially remove liquidity so there’s enough wS for the swap // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back - uint256 lpTokens = calcTokensToBurn(_wsAmount); + uint256 lpTokens = _calcTokensToBurn(_wsAmount); require(lpTokens > 0, "No LP tokens to burn"); _withdrawFromGaugeAndPool(lpTokens); @@ -355,6 +367,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { onlyStrategist nonReentrant improvePoolBalance + skimPool { require(_osAmount > 0, "Must swap some OS"); @@ -368,7 +381,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { require(_osAmount >= osInStrategy, "Too much OS in strategy"); uint256 osToMint = _osAmount - osInStrategy; - // Mint the required OS tokens to the strategy + // Mint the required OS tokens to this strategy IVault(vaultAddress).mintForStrategy(osToMint); // 2. Swap OS for wS against the pool @@ -378,11 +391,10 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // The wS is from the swap and any wS that was sitting in the strategy uint256 wsLiquidity = IERC20(ws).balanceOf(address(this)); - // Calculate how much OS liquidity is required from the wS liquidity - (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); - uint256 osLiquidity = (wsLiquidity * osReserves) / wsReserves; + // Calculate the required amount of OS to mint based on the wS amount. + uint256 osLiquidity = _calcTokensToMint(wsLiquidity); - // Mint more OS so it can be added to the pool + // Mint more OS to this strategy so they can then be added to the pool IVault(vaultAddress).mintForStrategy(osLiquidity); // Add wS and OS liquidity to the pool and stake in gauge @@ -395,10 +407,36 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { } /*************************************** - Pool Handling + Internal SwapX Pool and Gauge Functions ****************************************/ - function calcTokensToBurn(uint256 _wsAmount) + /** + * @dev Calculate the required amount of OS to mint based on the wS amount. + * This ensures the proportion of OS tokens being added to the pool matches the proportion of wS tokens. + * For example, if the added wS tokens is 10% of existing wS tokens in the pool, + * then the OS tokens being added should also be 10% of the OS tokens in the pool. + * @param _wsAmount Amount of Wrapped S (wS) to be added to the pool. + * @return osAmount Amount of OS to be minted and added to the pool. + */ + function _calcTokensToMint(uint256 _wsAmount) + internal + view + returns (uint256 osAmount) + { + (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); + require(wsReserves > 0, "Empty pool"); + + // OS to add = (wS being added * OS in pool) / wS in pool + osAmount = (_wsAmount * osReserves) / wsReserves; + } + + /** + * @dev Calculate how much pool LP tokens to burn to get the required amount of wS tokens back + * from the pool. + * @param _wsAmount Amount of Wrapped S (wS) to be removed from the pool. + * @return lpTokens Amount of SwapX pool LP tokens to burn. + */ + function _calcTokensToBurn(uint256 _wsAmount) internal view returns (uint256 lpTokens) @@ -412,23 +450,31 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * * Important: A downside is that the Strategist / Governor needs to be * cognizant of not removing too much liquidity. And while the proposal to remove liquidity - * is being voted on the pool tilt might change so much that the proposal that has been valid while + * is being voted on, the pool tilt might change so much that the proposal that has been valid while * created is no longer valid. */ - lpTokens = - ((_wsAmount + 1) * IPair(pool).totalSupply()) / - IERC20(ws).balanceOf(pool); + (uint256 wsReserves, , ) = IPair(pool).getReserves(); + require(wsReserves > 0, "Empty pool"); + + lpTokens = ((_wsAmount + 1) * IPair(pool).totalSupply()) / wsReserves; } + /** + * @dev Deposit Wrapped S (wS) and OS liquidity to the SwapX pool + * and stake the pool's LP token in the gauge. + * @param _wsAmount Amount of Wrapped S (wS) to deposit. + * @param osAmount Amount of OS to deposit. + * @return lpTokens Amount of SwapX pool LP tokens minted. + */ function _depositToPoolAndGauge(uint256 _wsAmount, uint256 osAmount) internal returns (uint256 lpTokens) { // Transfer wS to the pool - IERC20(ws).transfer(pool, _wsAmount); + IERC20(ws).safeTransfer(pool, _wsAmount); // Transfer OS to the pool - IERC20(os).transfer(pool, osAmount); + IERC20(os).safeTransfer(pool, osAmount); // Mint LP tokens from the pool lpTokens = IPair(pool).mint(address(this)); @@ -448,18 +494,24 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IGauge(gauge).withdraw(lpTokens); // Transfer the pool LP tokens to the pool - IPair(pool).transfer(pool, lpTokens); + IERC20(pool).safeTransfer(pool, lpTokens); // Burn the LP tokens and transfer the wS and OS back to the strategy IPair(pool).burn(address(this)); } + /** + * @dev Swap exact amount of tokens for another token against the pool. + * @param _amountIn Amount of tokens to swap into the pool. + * @param _tokenIn Address of the token going into the pool. + * @param _tokenOut Address of the token being swapped out of the pool. + */ function _swapExactTokensForTokens( uint256 _amountIn, address _tokenIn, address _tokenOut ) internal { // Transfer in tokens to the pool - IERC20(_tokenIn).transfer(pool, _amountIn); + IERC20(_tokenIn).safeTransfer(pool, _amountIn); // Calculate how much out tokens we get from the swap uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); @@ -480,12 +532,14 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IPair(pool).swap(amount0, amount1, address(this), new bytes(0)); // The slippage protection against the amount out is indirectly done - // via the improvePoolBalance and strategyValueChecker modifiers + // via the improvePoolBalance } - /// @notice Calculate the value of a LP position in a SwapX stable pool - /// if the pool was balanced - function lpValue(uint256 lpTokens) public view returns (uint256) { + /// @dev Calculate the value of a LP position in a SwapX stable pool + /// if the pool was balanced. + /// @param lpTokens Amount of LP tokens in the SwapX pool + /// @return The wS value of the LP tokens in the pool + function _lpValue(uint256 lpTokens) internal view returns (uint256) { // Get total supply of LP tokens uint256 totalSupply = IPair(pool).totalSupply(); require(totalSupply > 0, "No liquidity in pool"); @@ -513,8 +567,11 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { } /// @dev Compute the invariant for a SwapX stable pool. - /// This assumed both x and y tokens are to 18 decimals. - // k = x^3 * y + x * y^3 + /// This assumed both x and y tokens are to 18 decimals which is checked in the constructor. + /// invariant: k = x^3 * y + x * y^3 + /// @param x The amount of Wrapped S (wS) tokens in the pool + /// @param y The amount of the OS tokens in the pool + /// @return The invariant k of the pool function _invariant(uint256 x, uint256 y) public pure returns (uint256) { uint256 _a = (x * y) / PRECISION; uint256 _b = ((x * x) / PRECISION + (y * y) / PRECISION); @@ -522,7 +579,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { } /** - * Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can + * @dev Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can * keep rebalancing the pool in both directions making the protocol lose a tiny amount of * funds each time. * @@ -579,8 +636,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); if (lpTokens == 0) return balance; - // Add the strategy’s share of the wS and OS tokens in the SwapX pool. - balance += lpValue(lpTokens); + // Add the strategy’s share of the wS and OS tokens in the SwapX pool if the pool was balanced. + balance += _lpValue(lpTokens); } /** @@ -615,11 +672,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { {} function _approveBase() internal { - // Approve pool for OS (required for adding liquidity) + // Approve pool for OS and wS (required for adding liquidity) // slither-disable-next-line unused-return IERC20(os).approve(platformAddress, type(uint256).max); - - // Approve SwapX pool for wS (required for adding liquidity) // slither-disable-next-line unused-return IERC20(ws).approve(platformAddress, type(uint256).max); @@ -629,13 +684,6 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IPair(pool).approve(address(gauge), type(uint256).max); } - /** - * @dev Returns the largest of two numbers int256 version - */ - function _max(int256 a, int256 b) internal pure returns (int256) { - return a >= b ? a : b; - } - /// @dev including in here for now. This is a copy of PRBMath's sqrt function. /// Can either move out into a new lib or import from the use the PRBMath project From c19a5ced62871f7dc01ecb5ceea4911c71ca2f97 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 6 Mar 2025 22:33:09 +1100 Subject: [PATCH 62/80] Polish the SwapX AMO contract --- .../sonic/SonicSwapXAMOStrategy.sol | 274 ++++++------------ contracts/contracts/utils/PRBMath.sol | 95 ++++++ .../sonic/swapx-amo.sonic.fork-test.js | 4 +- 3 files changed, 191 insertions(+), 182 deletions(-) create mode 100644 contracts/contracts/utils/PRBMath.sol diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index b34116bd21..cea294f303 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -11,10 +11,11 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { StableMath } from "../../utils/StableMath.sol"; +import { sqrt } from "../../utils/PRBMath.sol"; import { IBasicToken } from "../../interfaces/IBasicToken.sol"; -import { IVault } from "../../interfaces/IVault.sol"; import { IPair } from "../../interfaces/sonic/ISwapXPair.sol"; import { IGauge } from "../../interfaces/sonic/ISwapXGauge.sol"; +import { IVault } from "../../interfaces/IVault.sol"; contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { using SafeERC20 for IERC20; @@ -27,28 +28,20 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { * draining the protocol funds. */ uint256 public constant SOLVENCY_THRESHOLD = 0.998 ether; - /// @notice Precision for the SwapX Stable AMM (sAMM) invariant + + /// @notice Precision for the SwapX Stable AMM (sAMM) invariant k. uint256 public constant PRECISION = 1e18; - // New immutable variables that must be set in the constructor - /** - * @notice Address of the Wrapped S (wS) token. - */ + /// @notice Address of the Wrapped S (wS) token. address public immutable ws; - /** - * @notice Address of the OS token contract. - */ + /// @notice Address of the OS token contract. address public immutable os; - /** - * @notice Address of the SwapX Stable pool contract. - */ + /// @notice Address of the SwapX Stable pool contract. address public immutable pool; - /** - * @notice Address of the SwapX Gauge contract. - */ + /// @notice Address of the SwapX Gauge contract. address public immutable gauge; event SwapOTokensToPool( @@ -120,6 +113,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { } } + /** + * @param _baseConfig The `platformAddress` is the address of the SwapX pool. + * The `vaultAddress` is the address of the Origin Sonic Vault. + * @param _os Address of the OS token. + * @param _ws Address of the Wrapped S (wS) token. + * @param _gauge Address of the SwapX gauge for the pool. + */ constructor( BaseStrategyConfig memory _baseConfig, address _os, @@ -320,11 +320,12 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { Pool Rebalancing ****************************************/ - /// @notice Used when there is more OS than wS in the pool. - /// wS and OS is removed from the pool, the received wS is swapped for OS - /// and the left over OS in the strategy is burnt. - /// The OS/wS price is < 1.0 so OS is being bought at a discount. - /// @param _wsAmount Amount of Wrapped S (wS) to swap into the pool. + /** @notice Used when there is more OS than wS in the pool. + * wS and OS is removed from the pool, the received wS is swapped for OS + * and the left over OS in the strategy is burnt. + * The OS/wS price is < 1.0 so OS is being bought at a discount. + * @param _wsAmount Amount of Wrapped S (wS) to swap into the pool. + */ function swapAssetsToPool(uint256 _wsAmount) external onlyStrategist @@ -332,7 +333,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { improvePoolBalance skimPool { - require(_wsAmount > 0, "Must swap some wS"); + require(_wsAmount > 0, "Must swap something"); // 1. Partially remove liquidity so there’s enough wS for the swap @@ -358,10 +359,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { emit SwapAssetsToPool(wsInStrategy, lpTokens, osToBurn); } - /// @notice Used when there is more wS than OS in the pool. - /// OS is minted and swapped for wS against the pool, - /// more OS is minted and added back into the pool with the swapped out wS. - /// The OS/wS price is > 1.0 so OS is being sold at a premium. + /** + * @notice Used when there is more wS than OS in the pool. + * OS is minted and swapped for wS against the pool, + * more OS is minted and added back into the pool with the swapped out wS. + * The OS/wS price is > 1.0 so OS is being sold at a premium. + * @param _osAmount Amount of OS to swap into the pool. + */ function swapOTokensToPool(uint256 _osAmount) external onlyStrategist @@ -369,10 +373,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { improvePoolBalance skimPool { - require(_osAmount > 0, "Must swap some OS"); - - // Skim the pool in case extra tokens were added - IPair(pool).skim(address(this)); + require(_osAmount > 0, "Must swap something"); // 1. Mint OS so it can be swapped into the pool @@ -406,6 +407,59 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { emit SwapOTokensToPool(osToMint, wsLiquidity, osLiquidity, lpTokens); } + /*************************************** + Assets and Rewards + ****************************************/ + + /** + * @notice Get the wS value of assets in the strategy and SwapX pool. + * The value of the assets in the pool is calculated assuming the pool is balanced. + * This way the value can not be manipulated by changing the pool's token balances. + * @param _asset Address of the Wrapped S (wS) token + * @return balance Total value in wS. + */ + function checkBalance(address _asset) + public + view + override + returns (uint256 balance) + { + require(_asset == ws, "Unsupported asset"); + + // wS balance needed here for the balance check that happens from vault during depositing. + balance = IERC20(ws).balanceOf(address(this)); + + // This assumes 1 gauge LP token = 1 pool LP token + uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); + if (lpTokens == 0) return balance; + + // Add the strategy’s share of the wS and OS tokens in the SwapX pool if the pool was balanced. + balance += _lpValue(lpTokens); + } + + /** + * @notice Returns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + */ + function supportsAsset(address _asset) public view override returns (bool) { + return _asset == ws; + } + + /** + * @notice Collect accumulated SWPx (and other) rewards and send to the Harvester. + */ + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + // Collect SWPx rewards from the gauge + IGauge(gauge).getReward(); + + _collectRewardTokens(); + } + /*************************************** Internal SwapX Pool and Gauge Functions ****************************************/ @@ -483,7 +537,10 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IGauge(gauge).deposit(lpTokens); } - /// @param lpTokens Amount of SwapX pool LP tokens to withdraw from the gauge + /** + * @dev Withdraw pool LP tokens from the gauge and remove wS and OS from the pool. + * @param lpTokens Amount of SwapX pool LP tokens to withdraw from the gauge + */ function _withdrawFromGaugeAndPool(uint256 lpTokens) internal { require( IGauge(gauge).balanceOf(address(this)) >= lpTokens, @@ -495,6 +552,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Transfer the pool LP tokens to the pool IERC20(pool).safeTransfer(pool, lpTokens); + // Burn the LP tokens and transfer the wS and OS back to the strategy IPair(pool).burn(address(this)); } @@ -538,8 +596,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { /// @dev Calculate the value of a LP position in a SwapX stable pool /// if the pool was balanced. /// @param lpTokens Amount of LP tokens in the SwapX pool - /// @return The wS value of the LP tokens in the pool - function _lpValue(uint256 lpTokens) internal view returns (uint256) { + /// @return value The wS value of the LP tokens in the pool + function _lpValue(uint256 lpTokens) internal view returns (uint256 value) { // Get total supply of LP tokens uint256 totalSupply = IPair(pool).totalSupply(); require(totalSupply > 0, "No liquidity in pool"); @@ -563,7 +621,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { uint256 totalValueOfPool = 2 * z; // lp value = lp tokens * value of pool / total supply - return (lpTokens * totalValueOfPool) / totalSupply; + value = (lpTokens * totalValueOfPool) / totalSupply; } /// @dev Compute the invariant for a SwapX stable pool. @@ -571,11 +629,11 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { /// invariant: k = x^3 * y + x * y^3 /// @param x The amount of Wrapped S (wS) tokens in the pool /// @param y The amount of the OS tokens in the pool - /// @return The invariant k of the pool - function _invariant(uint256 x, uint256 y) public pure returns (uint256) { + /// @return k The invariant of the SwapX stable pool + function _invariant(uint256 x, uint256 y) public pure returns (uint256 k) { uint256 _a = (x * y) / PRECISION; uint256 _b = ((x * x) / PRECISION + (y * y) / PRECISION); - return (_a * _b) / PRECISION; + k = (_a * _b) / PRECISION; } /** @@ -597,57 +655,6 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { } } - /*************************************** - Assets and Rewards - ****************************************/ - - /** - * @notice Collect accumulated SWPx (and other) rewards and send to the Harvester. - */ - function collectRewardTokens() - external - override - onlyHarvester - nonReentrant - { - // Collect SWPx rewards from the gauge - IGauge(gauge).getReward(); - - _collectRewardTokens(); - } - - /** - * @notice Get the total asset value held in the SwapX pool - * @param _asset Address of the Wrapped S (wS) token - * @return balance Total value of the wS and OS tokens held in the pool - */ - function checkBalance(address _asset) - public - view - override - returns (uint256 balance) - { - require(_asset == ws, "Unsupported asset"); - - // wS balance needed here for the balance check that happens from vault during depositing. - balance = IERC20(ws).balanceOf(address(this)); - - // This assumes 1 gauge LP token = 1 pool LP token - uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); - if (lpTokens == 0) return balance; - - // Add the strategy’s share of the wS and OS tokens in the SwapX pool if the pool was balanced. - balance += _lpValue(lpTokens); - } - - /** - * @notice Returns bool indicating whether asset is supported by strategy - * @param _asset Address of the asset - */ - function supportsAsset(address _asset) public view override returns (bool) { - return _asset == ws; - } - /*************************************** Approvals ****************************************/ @@ -683,97 +690,4 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // slither-disable-next-line unused-return IPair(pool).approve(address(gauge), type(uint256).max); } - - /// @dev including in here for now. This is a copy of PRBMath's sqrt function. - /// Can either move out into a new lib or import from the use the PRBMath project - - /// @notice Calculates the square root of x using the Babylonian method. - /// - /// @dev See https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method. - /// - /// Notes: - /// - If x is not a perfect square, the result is rounded down. - /// - Credits to OpenZeppelin for the explanations in comments below. - /// - /// @param x The uint256 number for which to calculate the square root. - /// @return result The result as a uint256. - /// @custom:smtchecker abstract-function-nondet - function sqrt(uint256 x) public pure returns (uint256 result) { - if (x == 0) { - return 0; - } - - // For our first guess, we calculate the biggest power of 2 which is smaller than the square root of x. - // - // We know that the "msb" (most significant bit) of x is a power of 2 such that we have: - // - // $$ - // msb(x) <= x <= 2*msb(x)$ - // $$ - // - // We write $msb(x)$ as $2^k$, and we get: - // - // $$ - // k = log_2(x) - // $$ - // - // Thus, we can write the initial inequality as: - // - // $$ - // 2^{log_2(x)} <= x <= 2*2^{log_2(x)+1} \\ - // sqrt(2^k) <= sqrt(x) < sqrt(2^{k+1}) \\ - // 2^{k/2} <= sqrt(x) < 2^{(k+1)/2} <= 2^{(k/2)+1} - // $$ - // - // Consequently, $2^{log_2(x) /2} is a good first approximation of sqrt(x) with at least one correct bit. - uint256 xAux = uint256(x); - result = 1; - if (xAux >= 2**128) { - xAux >>= 128; - result <<= 64; - } - if (xAux >= 2**64) { - xAux >>= 64; - result <<= 32; - } - if (xAux >= 2**32) { - xAux >>= 32; - result <<= 16; - } - if (xAux >= 2**16) { - xAux >>= 16; - result <<= 8; - } - if (xAux >= 2**8) { - xAux >>= 8; - result <<= 4; - } - if (xAux >= 2**4) { - xAux >>= 4; - result <<= 2; - } - if (xAux >= 2**2) { - result <<= 1; - } - - // At this point, `result` is an estimation with at least one bit of precision. We know the true value has at - // most 128 bits, since it is the square root of a uint256. Newton's method converges quadratically (precision - // doubles at every iteration). We thus need at most 7 iteration to turn our partial result with one bit of - // precision into the expected uint128 result. - unchecked { - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - - // If x is not a perfect square, round the result toward zero. - uint256 roundedResult = x / result; - if (result >= roundedResult) { - result = roundedResult; - } - } - } } diff --git a/contracts/contracts/utils/PRBMath.sol b/contracts/contracts/utils/PRBMath.sol new file mode 100644 index 0000000000..090ce51ce4 --- /dev/null +++ b/contracts/contracts/utils/PRBMath.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Copied from the PRBMath library +// https://github.com/PaulRBerg/prb-math/blob/main/src/Common.sol + +/// @notice Calculates the square root of x using the Babylonian method. +/// +/// @dev See https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method. +/// +/// Notes: +/// - If x is not a perfect square, the result is rounded down. +/// - Credits to OpenZeppelin for the explanations in comments below. +/// +/// @param x The uint256 number for which to calculate the square root. +/// @return result The result as a uint256. +/// @custom:smtchecker abstract-function-nondet +function sqrt(uint256 x) pure returns (uint256 result) { + if (x == 0) { + return 0; + } + + // For our first guess, we calculate the biggest power of 2 which is smaller than the square root of x. + // + // We know that the "msb" (most significant bit) of x is a power of 2 such that we have: + // + // $$ + // msb(x) <= x <= 2*msb(x)$ + // $$ + // + // We write $msb(x)$ as $2^k$, and we get: + // + // $$ + // k = log_2(x) + // $$ + // + // Thus, we can write the initial inequality as: + // + // $$ + // 2^{log_2(x)} <= x <= 2*2^{log_2(x)+1} \\ + // sqrt(2^k) <= sqrt(x) < sqrt(2^{k+1}) \\ + // 2^{k/2} <= sqrt(x) < 2^{(k+1)/2} <= 2^{(k/2)+1} + // $$ + // + // Consequently, $2^{log_2(x) /2} is a good first approximation of sqrt(x) with at least one correct bit. + uint256 xAux = uint256(x); + result = 1; + if (xAux >= 2**128) { + xAux >>= 128; + result <<= 64; + } + if (xAux >= 2**64) { + xAux >>= 64; + result <<= 32; + } + if (xAux >= 2**32) { + xAux >>= 32; + result <<= 16; + } + if (xAux >= 2**16) { + xAux >>= 16; + result <<= 8; + } + if (xAux >= 2**8) { + xAux >>= 8; + result <<= 4; + } + if (xAux >= 2**4) { + xAux >>= 4; + result <<= 2; + } + if (xAux >= 2**2) { + result <<= 1; + } + + // At this point, `result` is an estimation with at least one bit of precision. We know the true value has at + // most 128 bits, since it is the square root of a uint256. Newton's method converges quadratically (precision + // doubles at every iteration). We thus need at most 7 iteration to turn our partial result with one bit of + // precision into the expected uint128 result. + unchecked { + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + result = (result + x / result) >> 1; + + // If x is not a perfect square, round the result toward zero. + uint256 roundedResult = x / result; + if (result >= roundedResult) { + result = roundedResult; + } + } +} diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index bc21ff2500..b2f57b7961 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -467,7 +467,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const tx = swapXAMOStrategy.connect(strategist).swapAssetsToPool(0); - await expect(tx).to.be.revertedWith("Must swap some wS"); + await expect(tx).to.be.revertedWith("Must swap something"); }); it("Strategist should fail to add more OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; @@ -550,7 +550,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const tx = swapXAMOStrategy.connect(strategist).swapOTokensToPool(0); - await expect(tx).to.be.revertedWith("Must swap some OS"); + await expect(tx).to.be.revertedWith("Must swap swap something"); }); it("Strategist should fail to add too much OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; From 41e271afed8d7cbe66a6e263ef9fb24a30350d0a Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 10:58:18 +1100 Subject: [PATCH 63/80] Got fork tests working again --- .../sonic/SonicSwapXAMOStrategy.sol | 6 +- .../sonic/swapx-amo.sonic.fork-test.js | 142 +++++------------- 2 files changed, 42 insertions(+), 106 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index cea294f303..79bb5c8690 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -630,7 +630,11 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { /// @param x The amount of Wrapped S (wS) tokens in the pool /// @param y The amount of the OS tokens in the pool /// @return k The invariant of the SwapX stable pool - function _invariant(uint256 x, uint256 y) public pure returns (uint256 k) { + function _invariant(uint256 x, uint256 y) + internal + pure + returns (uint256 k) + { uint256 _a = (x * y) / PRECISION; uint256 _b = ((x * x) / PRECISION + (y * y) / PRECISION); k = (_a * _b) / PRECISION; diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index b2f57b7961..6ab3b8c164 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -5,7 +5,6 @@ const { createFixtureLoader } = require("../../_fixture"); const { swapXAMOFixture } = require("../../_fixture-sonic"); const { isCI } = require("../../helpers"); const addresses = require("../../../utils/addresses"); -const { ethers } = require("hardhat"); const log = require("../../../utils/logger")("test:fork:sonic:swapx:amo"); @@ -182,92 +181,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - function sqrt(value) { - const ONE = ethers.BigNumber.from(1); - const TWO = ethers.BigNumber.from(2); - - const x = ethers.BigNumber.from(value); - let z = x.add(ONE).div(TWO); - let y = x; - while (z.sub(y).isNegative()) { - y = z; - z = x.div(z).add(z).div(TWO); - } - return y; - } - - describe("Pool testing", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 100, - depositToStrategy: true, - balancePool: true, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("current state", async () => { - const data = await snapData(fixture); - await logSnapData(data); - - const x = parseUnits("500"); - const y = parseUnits("500"); - const precision = parseUnits("1", 18); - const a = x.mul(y).div(precision); - const b = x.mul(x).div(precision).add(y.mul(y).div(precision)); - const k = a.mul(b).div(precision); - log(`local k for 50:50 = ${formatUnits(k)}`); - - const remoteK = await fixture.swapXAMOStrategy._invariant(x, y); - log(`remote k for 50:50 = ${formatUnits(remoteK)}`); - - const remoteKactual = await fixture.swapXAMOStrategy._invariant( - data.reserves.ws, - data.reserves.os - ); - log(`remote k actual = ${formatUnits(remoteKactual)}`); - - log(`local sqrt(2500) = ${sqrt(2500)}`); - const remoteSqrt125 = await fixture.swapXAMOStrategy.sqrt("2500"); - log(`remote sqrt(2500) = ${remoteSqrt125}`); - - const lpValue = await fixture.swapXAMOStrategy.lpValue(parseUnits("250")); - log(`LP value = ${formatUnits(lpValue)}`); - - const cubedPrecision = parseUnits("1", 54); - const z1 = sqrt(sqrt(k.mul(cubedPrecision).div(2))); - log(`first z = ${formatUnits(z1)}`); - - const adjustedK2 = k.mul(parseUnits("1", 36)).div(2); - const sqrt1 = sqrt(adjustedK2); - const z2 = sqrt(sqrt1.mul(parseUnits("1", 9))); - log(`second z = ${formatUnits(z2)}`); - - const adjustedK3 = k.mul(parseUnits("1", 18)).div(2); - const sqrt2 = sqrt(adjustedK3); - const z3 = sqrt(sqrt2.mul(parseUnits("1", 18))); - log(`third z = ${formatUnits(z3)}`); - - const adjustedK4 = k.mul(parseUnits("1", 12)).div(2); - const sqrt3 = sqrt(adjustedK4); - const z4 = sqrt(sqrt3.mul(parseUnits("1", 21))); - log(`fourth z = ${formatUnits(z4)}`); - - // const adjustedK4 = k.mul(parseUnits("1", 12)).div(2); - const sqrt4 = sqrt(k.div(2)); - const z5 = sqrt(sqrt4.mul(parseUnits("1", 9))); - log(`fifth z = ${formatUnits(z5.mul(parseUnits("1", 9)))}`); - - const val = sqrt(sqrt(k.mul(8)).mul(parseUnits("1", 9))); - log(`alt value = ${formatUnits(val)}`); - - const fiftySquared = ethers.BigNumber.from(2500); - const sqrtFiftySquared = sqrt(fiftySquared); - log(`sqrt 2,500 ${sqrtFiftySquared}`); - expect(sqrtFiftySquared).to.equal(50); - }); - }); - - describe("with the strategy having some OS and wS in the pool", () => { + describe("with the strategy having some OS and wS in a balanced pool", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: true, @@ -292,6 +206,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .connect(oSonicVaultSigner) .withdrawAll(); + await logSnapData(await snapData(fixture)); + // Check emitted events await expect(tx) .to.emit(swapXAMOStrategy, "Withdrawal") @@ -550,7 +466,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const tx = swapXAMOStrategy.connect(strategist).swapOTokensToPool(0); - await expect(tx).to.be.revertedWith("Must swap swap something"); + await expect(tx).to.be.revertedWith("Must swap something"); }); it("Strategist should fail to add too much OS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; @@ -623,16 +539,27 @@ const assertChangedData = async (dataBefore, delta, fixture) => { const expectedStratBalance = dataBefore.stratBalance.add( delta.stratBalance ); + // Truncate any dust amounts to zero + const expectedStratBalanceTruncated = expectedStratBalance + .div(100000) + .mul(100000); + log( + `expected strat balance : ${formatUnits(expectedStratBalanceTruncated)}` + ); expect( - await swapXAMOStrategy.checkBalance(wS.address), + await swapXAMOStrategy.checkBalance(wS.address) + ).to.approxEqualTolerance( + expectedStratBalanceTruncated, + "0.01", // 0.01% (10 bps) tolerance "Strategy's check balance" - ).to.withinRange(expectedStratBalance.sub(1), expectedStratBalance); + ); } if (delta.osSupply != undefined) { const expectedSupply = dataBefore.osSupply.add(delta.osSupply); - expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( - expectedSupply + expect(await oSonic.totalSupply()).to.equal( + expectedSupply, + "OSonic total supply" ); } @@ -640,11 +567,13 @@ const assertChangedData = async (dataBefore, delta, fixture) => { if (delta.reserves != undefined) { const { _reserve0: wsReserves, _reserve1: osReserves } = await swapXPool.getReserves(); - expect(wsReserves, "wS reserves").to.equal( - dataBefore.reserves.ws.add(delta.reserves.ws) + expect(wsReserves).to.equal( + dataBefore.reserves.ws.add(delta.reserves.ws), + "wS reserves" ); - expect(osReserves, "OS reserves").to.equal( - dataBefore.reserves.os.add(delta.reserves.os) + expect(osReserves).to.equal( + dataBefore.reserves.os.add(delta.reserves.os), + "OS reserves" ); // Check the strategy's gauge balance @@ -661,21 +590,19 @@ const assertChangedData = async (dataBefore, delta, fixture) => { const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( deltaStratGaugeBalance ); - expect( - await swapXGauge.balanceOf(swapXAMOStrategy.address), - "Strategy's gauge balance" - ).to.withinRange( + expect(await swapXGauge.balanceOf(swapXAMOStrategy.address)).to.withinRange( expectedStratGaugeBalance.sub(1), - expectedStratGaugeBalance.add(1) + expectedStratGaugeBalance.add(1), + "Strategy's gauge balance" ); } // Check Vault's wS balance if (delta.vaultWSBalance != undefined) { - expect( - await wS.balanceOf(oSonicVault.address), + expect(await wS.balanceOf(oSonicVault.address)).to.equal( + dataBefore.vaultWSBalance.add(delta.vaultWSBalance), "Vault's wS balance" - ).to.equal(dataBefore.vaultWSBalance.add(delta.vaultWSBalance)); + ); } }; @@ -693,7 +620,12 @@ async function assertSwapAssetsToPool(wsAmount, fixture) { await expect(tx).to.emittedEvent("SwapAssetsToPool", [ wsAmount, (lpTokens) => { - expect(lpTokens).to.approxEqualTolerance(wsAmount, 5); + // TODO better calculate the value of LP tokens + expect(lpTokens).to.approxEqualTolerance( + wsAmount, + 10, + "SwapAssetsToPool lpTokens" + ); }, (osBurnt) => { // TODO narrow down the range From 66d2bb3f6e9f1f898f3e20f3beddd7a65a3ce4f6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 15:43:02 +1100 Subject: [PATCH 64/80] Improved SwapX AMO fork tests --- .../sonic/swapx-amo.sonic.fork-test.js | 766 ++++++++++++------ 1 file changed, 525 insertions(+), 241 deletions(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 6ab3b8c164..7125f03d2c 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -82,7 +82,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - describe("with some wS in the vault", () => { + describe("with wS in the vault", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: false, @@ -91,11 +91,11 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { beforeEach(async () => { fixture = await loadFixture(); }); - it("Vault should deposit some wS to AMO strategy", async function () { + it("Vault should deposit wS to AMO strategy", async function () { const { swapXAMOStrategy, oSonic, swapXPool, oSonicVaultSigner, wS } = fixture; - const dataBefore = await snapData(fixture); + const dataBefore = await snapData(); await logSnapData(dataBefore); const wsDepositAmount = await parseUnits("5000"); @@ -118,18 +118,14 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .to.emit(swapXAMOStrategy, "Deposit") .withArgs(oSonic.address, swapXPool.address, osMintAmount); - await assertChangedData( - dataBefore, - { - stratBalance: wsDepositAmount.add(osMintAmount).sub(1), - osSupply: osMintAmount, - reserves: { ws: wsDepositAmount, os: osMintAmount }, - vaultWSBalance: wsDepositAmount.mul(-1), - }, - fixture - ); + await assertChangedData(dataBefore, { + stratBalance: wsDepositAmount.add(osMintAmount), + osSupply: osMintAmount, + reserves: { ws: wsDepositAmount, os: osMintAmount }, + vaultWSBalance: wsDepositAmount.mul(-1), + }); }); - it("Only vault can deposit some wS to AMO strategy", async function () { + it("Only vault can deposit wS to AMO strategy", async function () { const { swapXAMOStrategy, oSonicVaultSigner, @@ -181,7 +177,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - describe("with the strategy having some OS and wS in a balanced pool", () => { + describe("with the strategy having OS and wS in a balanced pool", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000, depositToStrategy: true, @@ -190,23 +186,64 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { beforeEach(async () => { fixture = await loadFixture(); }); + it("Vault should deposit wS to AMO strategy", async function () { + const { + clement, + swapXAMOStrategy, + oSonic, + oSonicVault, + swapXPool, + oSonicVaultSigner, + wS, + } = fixture; + + const wsDepositAmount = await parseUnits("5000"); + await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); + + const dataBefore = await snapData(); + await logSnapData(dataBefore); + + const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); + + // Vault transfers wS to strategy + await wS + .connect(oSonicVaultSigner) + .transfer(swapXAMOStrategy.address, wsDepositAmount); + // Vault calls deposit on the strategy + const tx = await swapXAMOStrategy + .connect(oSonicVaultSigner) + .deposit(wS.address, wsDepositAmount); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(wS.address, swapXPool.address, wsDepositAmount); + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(oSonic.address, swapXPool.address, osMintAmount); + + await assertChangedData(dataBefore, { + stratBalance: wsDepositAmount.add(osMintAmount), + osSupply: osMintAmount, + reserves: { ws: wsDepositAmount, os: osMintAmount }, + vaultWSBalance: wsDepositAmount.mul(-1), + }); + }); it("Vault should be able to withdraw all", async () => { const { swapXAMOStrategy, swapXPool, oSonic, oSonicVaultSigner, wS } = fixture; - const dataBefore = await snapData(fixture); + const dataBefore = await snapData(); await logSnapData(dataBefore); - const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts( - fixture - ); + const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts(); // Now try to withdraw all the wS from the strategy const tx = await swapXAMOStrategy .connect(oSonicVaultSigner) .withdrawAll(); - await logSnapData(await snapData(fixture)); + await logSnapData(await snapData()); // Check emitted events await expect(tx) @@ -216,18 +253,14 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .to.emit(swapXAMOStrategy, "Withdrawal") .withArgs(oSonic.address, swapXPool.address, osBurnAmount); - await assertChangedData( - dataBefore, - { - stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), - osSupply: osBurnAmount.mul(-1), - reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, - vaultWSBalance: wsWithdrawAmount, - }, - fixture - ); + await assertChangedData(dataBefore, { + stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), + osSupply: osBurnAmount.mul(-1), + reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, + vaultWSBalance: wsWithdrawAmount, + }); }); - it("Vault should be able to withdraw some", async () => { + it("Vault should be able to partially withdraw", async () => { const { swapXAMOStrategy, oSonic, @@ -237,13 +270,10 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { wS, } = fixture; - const dataBefore = await snapData(fixture); + const dataBefore = await snapData(); const wsWithdrawAmount = parseUnits("1000"); - const osBurnAmount = await calcOSWithdrawAmount( - fixture, - wsWithdrawAmount - ); + const osBurnAmount = await calcOSWithdrawAmount(wsWithdrawAmount); // Now try to withdraw the wS from the strategy const tx = await swapXAMOStrategy @@ -259,21 +289,17 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { _pToken: swapXPool.address, }); - await assertChangedData( - dataBefore, - { - stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), - osSupply: osBurnAmount.mul(-1), - reserves: { - ws: wsWithdrawAmount.mul(-1), - os: osBurnAmount.mul(-1), - }, - vaultWSBalance: wsWithdrawAmount, + await assertChangedData(dataBefore, { + stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), + osSupply: osBurnAmount.mul(-1), + reserves: { + ws: wsWithdrawAmount.mul(-1), + os: osBurnAmount.mul(-1), }, - fixture - ); + vaultWSBalance: wsWithdrawAmount, + }); }); - it("Only vault can withdraw some WETH from AMO strategy", async function () { + it("Only vault can withdraw wS from AMO strategy", async function () { const { swapXAMOStrategy, oSonicVault, strategist, timelock, nick, wS } = fixture; @@ -310,15 +336,70 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { beforeEach(async () => { fixture = await loadFixture(); }); + it("Vault should deposit wS to AMO strategy", async function () { + const { + clement, + swapXAMOStrategy, + oSonic, + oSonicVault, + swapXPool, + oSonicVaultSigner, + wS, + } = fixture; + + const wsDepositAmount = await parseUnits("5000"); + await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); + + const dataBefore = await snapData(); + await logSnapData(dataBefore, "Before depositing wS to strategy"); + + const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); + + // Vault transfers wS to strategy + await wS + .connect(oSonicVaultSigner) + .transfer(swapXAMOStrategy.address, wsDepositAmount); + // Vault calls deposit on the strategy + const tx = await swapXAMOStrategy + .connect(oSonicVaultSigner) + .deposit(wS.address, wsDepositAmount); + + await logSnapData( + await snapData(), + `After depositing ${formatUnits(wsDepositAmount)} wS to strategy` + ); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(wS.address, swapXPool.address, wsDepositAmount); + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(oSonic.address, swapXPool.address, osMintAmount); + + // Calculate the value of the wS and OS tokens added to the pool if the pool was balanced + const depositValue = calcReserveValue({ + ws: wsDepositAmount, + os: osMintAmount, + }); + log(`Value of deposit: ${formatUnits(depositValue)}`); + + await assertChangedData(dataBefore, { + stratBalance: depositValue, + osSupply: osMintAmount, + reserves: { ws: wsDepositAmount, os: osMintAmount }, + vaultWSBalance: wsDepositAmount.mul(-1), + }); + }); it("Strategist should swap a little assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3"), fixture); + await assertSwapAssetsToPool(parseUnits("3")); }); it("Strategist should swap a lot of assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3000"), fixture); + await assertSwapAssetsToPool(parseUnits("3000")); }); it("Strategist should swap most of the wS owned by the strategy", async () => { // TODO calculate how much wS should be swapped to get the pool balanced - await assertSwapAssetsToPool(parseUnits("4400"), fixture); + await assertSwapAssetsToPool(parseUnits("4400")); }); it("Strategist should fail to swap all wS owned by the strategy", async () => { const { swapXAMOStrategy, strategist } = fixture; @@ -361,12 +442,12 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Strategist should swap a little assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3"), fixture); + await assertSwapAssetsToPool(parseUnits("3")); }); it("Strategist should swap enough wS to get the pool close to balanced", async () => { // just under half the extra OS amount const osAmount = parseUnits("247"); - await assertSwapAssetsToPool(osAmount, fixture); + await assertSwapAssetsToPool(osAmount); }); it("Strategist should fail to add too much wS to the pool", async () => { const { swapXAMOStrategy, strategist } = fixture; @@ -407,6 +488,56 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { beforeEach(async () => { fixture = await loadFixture(); }); + it("Vault should deposit wS to AMO strategy", async function () { + const { + clement, + swapXAMOStrategy, + oSonic, + oSonicVault, + swapXPool, + oSonicVaultSigner, + wS, + } = fixture; + + const wsDepositAmount = await parseUnits("5000"); + await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); + + const dataBefore = await snapData(); + await logSnapData(dataBefore); + + const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); + + // Vault transfers wS to strategy + await wS + .connect(oSonicVaultSigner) + .transfer(swapXAMOStrategy.address, wsDepositAmount); + // Vault calls deposit on the strategy + const tx = await swapXAMOStrategy + .connect(oSonicVaultSigner) + .deposit(wS.address, wsDepositAmount); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(wS.address, swapXPool.address, wsDepositAmount); + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(oSonic.address, swapXPool.address, osMintAmount); + + // Calculate the value of the wS and OS tokens added to the pool if the pool was balanced + const depositValue = calcReserveValue({ + ws: wsDepositAmount, + os: osMintAmount, + }); + log(`Value of deposit: ${formatUnits(depositValue)}`); + + await assertChangedData(dataBefore, { + stratBalance: depositValue, + osSupply: osMintAmount, + reserves: { ws: wsDepositAmount, os: osMintAmount }, + vaultWSBalance: wsDepositAmount.mul(-1), + }); + }); it("Strategist should add a little OS to the pool", async () => { const osAmount = parseUnits("0.3"); await assertSwapOTokensToPool(osAmount, fixture); @@ -489,223 +620,376 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await expect(tx).to.be.revertedWith("Assets balance worse"); }); }); -}); -const snapData = async (fixture) => { - const { oSonicVault, swapXAMOStrategy, oSonic, swapXPool, swapXGauge, wS } = - fixture; - - const stratBalance = await swapXAMOStrategy.checkBalance(wS.address); - const osSupply = await oSonic.totalSupply(); - const poolSupply = await swapXPool.totalSupply(); - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - const stratGaugeBalance = await swapXGauge.balanceOf( - swapXAMOStrategy.address - ); - const gaugeSupply = await swapXGauge.totalSupply(); - const vaultWSBalance = await wS.balanceOf(oSonicVault.address); - const stratWSBalance = await wS.balanceOf(swapXAMOStrategy.address); - - return { - stratBalance, - osSupply, - poolSupply, - reserves: { ws: wsReserves, os: osReserves }, - stratGaugeBalance, - gaugeSupply, - vaultWSBalance, - stratWSBalance, + describe("with the strategy owning a small percentage of the pool", () => { + const loadFixture = createFixtureLoader(swapXAMOFixture, { + wsMintAmount: 5000, + depositToStrategy: true, + balancePool: true, + }); + let dataBefore; + beforeEach(async () => { + fixture = await loadFixture(); + + const { clement, wS, oSonic, oSonicVault, swapXPool } = fixture; + + // Other users adds a lot more liquidity to the pool + const bigAmount = parseUnits("1000000"); + // transfer wS to the pool + await wS.connect(clement).transfer(swapXPool.address, bigAmount); + // Mint OS with wS + await oSonicVault.connect(clement).mint(wS.address, bigAmount.mul(5), 0); + // transfer OS to the pool + await oSonic.connect(clement).transfer(swapXPool.address, bigAmount); + // mint pool LP tokens + await swapXPool.connect(clement).mint(clement.address); + + dataBefore = await snapData(); + await logSnapData(dataBefore); + }); + it("a lot of OS is swapped into the pool", async () => { + const { oSonic, swapXAMOStrategy, wS } = fixture; + + // Swap OS into the pool and wS out + await poolSwapTokensIn(oSonic, parseUnits("1005000")); + + await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); + + // Assert the strategy's balance + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( + dataBefore.stratBalance, + "Strategy's check balance" + ); + + // Swap wS into the pool and OS out + await poolSwapTokensIn(wS, parseUnits("2000000")); + + await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); + + // Assert the strategy's balance + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( + dataBefore.stratBalance, + "Strategy's check balance" + ); + }); + it("a lot of wS is swapped into the pool", async () => { + const { swapXAMOStrategy, oSonic, wS } = fixture; + + // Swap wS into the pool and OS out + await poolSwapTokensIn(wS, parseUnits("1006000")); + + await logSnapData(await snapData(), "After swapping wS into the pool"); + + // Assert the strategy's balance + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( + dataBefore.stratBalance, + "Strategy's check balance" + ); + + // Swap OS into the pool and wS out + await poolSwapTokensIn(oSonic, parseUnits("1005000")); + + await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); + + // Assert the strategy's balance + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( + dataBefore.stratBalance, + "Strategy's check balance" + ); + }); + }); + + const poolSwapTokensIn = async (tokenIn, amountIn) => { + const { clement, swapXPool, wS } = fixture; + const amountOut = await swapXPool.getAmountOut(amountIn, tokenIn.address); + await tokenIn.connect(clement).transfer(swapXPool.address, amountIn); + if (tokenIn.address == wS.address) { + await swapXPool.swap(0, amountOut, clement.address, "0x"); + } else { + await swapXPool.swap(amountOut, 0, clement.address, "0x"); + } }; -}; -const logSnapData = async (data) => { - log(`Strategy balance : ${formatUnits(data.stratBalance)}`); - log(`OS supply : ${formatUnits(data.osSupply)}`); - log(`pool supply : ${formatUnits(data.poolSupply)}`); - log(`reserves wS : ${formatUnits(data.reserves.ws)}`); - log(`reserves OS : ${formatUnits(data.reserves.os)}`); - log(`strat gauge balance : ${formatUnits(data.stratGaugeBalance)}`); - log(`gauge supply : ${formatUnits(data.gaugeSupply)}`); - log(`vault wS balance : ${formatUnits(data.vaultWSBalance)}`); - log(`strat wS balance : ${formatUnits(data.stratWSBalance)}`); -}; + const precision = parseUnits("1", 18); + // Calculate the value of wS and OS tokens assuming the pool is balanced + const calcReserveValue = (reserves) => { + const k = calcInvariant(reserves); + + // If x = y, let’s denote x = y = z (where z is the common reserve value) + // Substitute z into the invariant: + // k = z^3 * z + z * z^3 + // k = 2 * z^4 + // Going back the other way to calculate the common reserve value z + // z = (k / 2) ^ (1/4) + // the total value of the pool when x = y is 2 * z, which is 2 * (k / 2) ^ (1/4) + const zSquared = sqrt(k.mul(precision).div(2)); + const z = sqrt(zSquared.mul(precision)); + return z.mul(2); + }; -const assertChangedData = async (dataBefore, delta, fixture) => { - const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = - fixture; + const calcInvariant = (reserves) => { + const x = reserves.ws; + const y = reserves.os; + const a = x.mul(y).div(precision); + const b = x.mul(x).div(precision).add(y.mul(y).div(precision)); + const k = a.mul(b).div(precision); - if (delta.stratBalance != undefined) { - const expectedStratBalance = dataBefore.stratBalance.add( - delta.stratBalance - ); - // Truncate any dust amounts to zero - const expectedStratBalanceTruncated = expectedStratBalance - .div(100000) - .mul(100000); - log( - `expected strat balance : ${formatUnits(expectedStratBalanceTruncated)}` - ); - expect( - await swapXAMOStrategy.checkBalance(wS.address) - ).to.approxEqualTolerance( - expectedStratBalanceTruncated, - "0.01", // 0.01% (10 bps) tolerance - "Strategy's check balance" - ); - } + log(`Invariant: ${formatUnits(k)}`); - if (delta.osSupply != undefined) { - const expectedSupply = dataBefore.osSupply.add(delta.osSupply); - expect(await oSonic.totalSupply()).to.equal( - expectedSupply, - "OSonic total supply" - ); + return k; + }; + + function sqrt(value) { + const ONE = ethers.BigNumber.from(1); + const TWO = ethers.BigNumber.from(2); + + const x = ethers.BigNumber.from(value); + let z = x.add(ONE).div(TWO); + let y = x; + while (z.sub(y).isNegative()) { + y = z; + z = x.div(z).add(z).div(TWO); + } + return y; } - // Check the pool's reserves - if (delta.reserves != undefined) { + const snapData = async () => { + const { oSonicVault, swapXAMOStrategy, oSonic, swapXPool, swapXGauge, wS } = + fixture; + + const stratBalance = await swapXAMOStrategy.checkBalance(wS.address); + const osSupply = await oSonic.totalSupply(); + const poolSupply = await swapXPool.totalSupply(); const { _reserve0: wsReserves, _reserve1: osReserves } = await swapXPool.getReserves(); - expect(wsReserves).to.equal( - dataBefore.reserves.ws.add(delta.reserves.ws), - "wS reserves" - ); - expect(osReserves).to.equal( - dataBefore.reserves.os.add(delta.reserves.os), - "OS reserves" + const stratGaugeBalance = await swapXGauge.balanceOf( + swapXAMOStrategy.address ); + const gaugeSupply = await swapXGauge.totalSupply(); + const vaultWSBalance = await wS.balanceOf(oSonicVault.address); + const stratWSBalance = await wS.balanceOf(swapXAMOStrategy.address); + + return { + stratBalance, + osSupply, + poolSupply, + reserves: { ws: wsReserves, os: osReserves }, + stratGaugeBalance, + gaugeSupply, + vaultWSBalance, + stratWSBalance, + }; + }; - // Check the strategy's gauge balance - // Calculate the liquidity added to the pool - const wsLiquidity = delta.reserves.ws - .mul(dataBefore.poolSupply) - .div(dataBefore.reserves.ws); - const osLiquidity = delta.reserves.os - .mul(dataBefore.poolSupply) - .div(dataBefore.reserves.os); - const deltaStratGaugeBalance = wsLiquidity.lt(osLiquidity) - ? wsLiquidity - : osLiquidity; - const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( - deltaStratGaugeBalance - ); - expect(await swapXGauge.balanceOf(swapXAMOStrategy.address)).to.withinRange( - expectedStratGaugeBalance.sub(1), - expectedStratGaugeBalance.add(1), - "Strategy's gauge balance" - ); - } + const assertChangedData = async (dataBefore, delta) => { + const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = + fixture; - // Check Vault's wS balance - if (delta.vaultWSBalance != undefined) { - expect(await wS.balanceOf(oSonicVault.address)).to.equal( - dataBefore.vaultWSBalance.add(delta.vaultWSBalance), - "Vault's wS balance" - ); - } -}; + if (delta.stratBalance != undefined) { + const expectedStratBalance = dataBefore.stratBalance.add( + delta.stratBalance + ); + // Truncate any dust amounts to zero + const expectedStratBalanceTruncated = expectedStratBalance + .div(100000) + .mul(100000); + expect( + await swapXAMOStrategy.checkBalance(wS.address) + ).to.approxEqualTolerance( + expectedStratBalanceTruncated, + "0.01", // 0.01% (1 bps) tolerance + "Strategy's check balance" + ); + } -async function assertSwapAssetsToPool(wsAmount, fixture) { - const { swapXAMOStrategy, strategist } = fixture; - - const dataBefore = await snapData(fixture); - - // Swap wS to the pool and burn the received OS from the pool - const tx = await swapXAMOStrategy - .connect(strategist) - .swapAssetsToPool(wsAmount); - - // Check emitted event - await expect(tx).to.emittedEvent("SwapAssetsToPool", [ - wsAmount, - (lpTokens) => { - // TODO better calculate the value of LP tokens - expect(lpTokens).to.approxEqualTolerance( - wsAmount, - 10, - "SwapAssetsToPool lpTokens" + if (delta.osSupply != undefined) { + const expectedSupply = dataBefore.osSupply.add(delta.osSupply); + expect(await oSonic.totalSupply()).to.equal( + expectedSupply, + "OSonic total supply" + ); + } + + // Check the pool's reserves + if (delta.reserves != undefined) { + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + expect(wsReserves).to.equal( + dataBefore.reserves.ws.add(delta.reserves.ws), + "wS reserves" + ); + expect(osReserves).to.equal( + dataBefore.reserves.os.add(delta.reserves.os), + "OS reserves" ); - }, - (osBurnt) => { - // TODO narrow down the range - expect(osBurnt).to.gt(wsAmount, 1); - }, - ]); - - await assertChangedData( - dataBefore, - { - vaultWSBalance: 0, - }, - fixture - ); -} -async function assertSwapOTokensToPool(osAmount, fixture) { - const { swapXAMOStrategy, strategist } = fixture; + // Check the strategy's gauge balance + // Calculate the liquidity added to the pool + const wsLiquidity = delta.reserves.ws + .mul(dataBefore.poolSupply) + .div(dataBefore.reserves.ws); + const osLiquidity = delta.reserves.os + .mul(dataBefore.poolSupply) + .div(dataBefore.reserves.os); + const deltaStratGaugeBalance = wsLiquidity.lt(osLiquidity) + ? wsLiquidity + : osLiquidity; + const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( + deltaStratGaugeBalance + ); + expect( + await swapXGauge.balanceOf(swapXAMOStrategy.address) + ).to.withinRange( + expectedStratGaugeBalance.sub(1), + expectedStratGaugeBalance.add(1), + "Strategy's gauge balance" + ); + } - // Mint OS and swap into the pool, then mint more OS to add with the wS swapped out - const tx = await swapXAMOStrategy - .connect(strategist) - .swapOTokensToPool(osAmount); + // Check Vault's wS balance + if (delta.vaultWSBalance != undefined) { + expect(await wS.balanceOf(oSonicVault.address)).to.equal( + dataBefore.vaultWSBalance.add(delta.vaultWSBalance), + "Vault's wS balance" + ); + } + }; - // Check emitted event - await expect(tx) - .emit(swapXAMOStrategy, "SwapOTokensToPool") - .withNamedArgs({ osMinted: osAmount }); -} + async function assertSwapAssetsToPool(wsAmount) { + const { swapXAMOStrategy, strategist } = fixture; + + const dataBefore = await snapData(); + + // Swap wS to the pool and burn the received OS from the pool + const tx = await swapXAMOStrategy + .connect(strategist) + .swapAssetsToPool(wsAmount); + + // Check emitted event + await expect(tx).to.emittedEvent("SwapAssetsToPool", [ + wsAmount, + (lpTokens) => { + // TODO better calculate the value of LP tokens + expect(lpTokens).to.approxEqualTolerance( + wsAmount, + 10, + "SwapAssetsToPool lpTokens" + ); + }, + (osBurnt) => { + // TODO narrow down the range + expect(osBurnt).to.gt(wsAmount, 1); + }, + ]); + + await assertChangedData( + dataBefore, + { + vaultWSBalance: 0, + }, + fixture + ); + } -// Calculate the minted OS amount for a deposit -async function calcOSMintAmount(fixture, wsDepositAmount) { - const { swapXPool } = fixture; + async function assertSwapOTokensToPool(osAmount) { + const { swapXAMOStrategy, strategist } = fixture; - // Get the reserves of the pool - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); + // Mint OS and swap into the pool, then mint more OS to add with the wS swapped out + const tx = await swapXAMOStrategy + .connect(strategist) + .swapOTokensToPool(osAmount); - const osMintAmount = wsDepositAmount.mul(osReserves).div(wsReserves); - log(`OS mint amount : ${formatUnits(osMintAmount)}`); + // Check emitted event + await expect(tx) + .emit(swapXAMOStrategy, "SwapOTokensToPool") + .withNamedArgs({ osMinted: osAmount }); + } - return osMintAmount; -} + // Calculate the minted OS amount for a deposit + async function calcOSMintAmount(fixture, wsDepositAmount) { + const { swapXPool } = fixture; -// Calculate the amount of OS burnt from a withdraw -async function calcOSWithdrawAmount(fixture, wethWithdrawAmount) { - const { swapXPool } = fixture; + // Get the reserves of the pool + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); - // Get the reserves of the pool - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); + const osMintAmount = wsDepositAmount.mul(osReserves).div(wsReserves); + log(`OS mint amount : ${formatUnits(osMintAmount)}`); - // OS to burn = wS withdrawn * OS reserves / wS reserves - const osBurnAmount = wethWithdrawAmount.mul(osReserves).div(wsReserves); + return osMintAmount; + } - log(`OS burn amount : ${formatUnits(osBurnAmount)}`); + // Calculate the amount of OS burnt from a withdraw + async function calcOSWithdrawAmount(wethWithdrawAmount) { + const { swapXPool } = fixture; - return osBurnAmount; -} + // Get the reserves of the pool + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + + // OS to burn = wS withdrawn * OS reserves / wS reserves + const osBurnAmount = wethWithdrawAmount.mul(osReserves).div(wsReserves); -// Calculate the OS and wS amounts from a withdrawAll -async function calcWithdrawAllAmounts(fixture) { - const { swapXAMOStrategy, swapXGauge, swapXPool } = fixture; + log(`OS burn amount : ${formatUnits(osBurnAmount)}`); - // Get the reserves of the pool - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - const strategyLpAmount = await swapXGauge.balanceOf(swapXAMOStrategy.address); - const totalLpSupply = await swapXPool.totalSupply(); + return osBurnAmount; + } - // wS to withdraw = wS pool balance * strategy LP amount / total pool LP amount - const wsWithdrawAmount = wsReserves.mul(strategyLpAmount).div(totalLpSupply); - // OS to burn = OS pool balance * strategy LP amount / total pool LP amount - const osBurnAmount = osReserves.mul(strategyLpAmount).div(totalLpSupply); + // Calculate the OS and wS amounts from a withdrawAll + async function calcWithdrawAllAmounts() { + const { swapXAMOStrategy, swapXGauge, swapXPool } = fixture; - log(`wS withdraw amount : ${formatUnits(wsWithdrawAmount)}`); - log(`OS burn amount : ${formatUnits(osBurnAmount)}`); + // Get the reserves of the pool + const { _reserve0: wsReserves, _reserve1: osReserves } = + await swapXPool.getReserves(); + const strategyLpAmount = await swapXGauge.balanceOf( + swapXAMOStrategy.address + ); + const totalLpSupply = await swapXPool.totalSupply(); + + // wS to withdraw = wS pool balance * strategy LP amount / total pool LP amount + const wsWithdrawAmount = wsReserves + .mul(strategyLpAmount) + .div(totalLpSupply); + // OS to burn = OS pool balance * strategy LP amount / total pool LP amount + const osBurnAmount = osReserves.mul(strategyLpAmount).div(totalLpSupply); + + log(`wS withdraw amount : ${formatUnits(wsWithdrawAmount)}`); + log(`OS burn amount : ${formatUnits(osBurnAmount)}`); + + return { + wsWithdrawAmount, + osBurnAmount, + }; + } +}); - return { - wsWithdrawAmount, - osBurnAmount, +const logSnapData = async (data, message) => { + const totalReserves = data.reserves.ws.add(data.reserves.os); + const reserversPercentage = { + ws: data.reserves.ws.mul(10000).div(totalReserves), + os: data.reserves.os.mul(10000).div(totalReserves), }; -} + if (message) { + log(message); + } + log(`Strategy balance : ${formatUnits(data.stratBalance)}`); + log(`OS supply : ${formatUnits(data.osSupply)}`); + log(`pool supply : ${formatUnits(data.poolSupply)}`); + log( + `reserves wS : ${formatUnits(data.reserves.ws)} ${formatUnits( + reserversPercentage.ws, + 2 + )}%` + ); + log( + `reserves OS : ${formatUnits(data.reserves.os)} ${formatUnits( + reserversPercentage.os, + 2 + )}%` + ); + log(`strat gauge balance : ${formatUnits(data.stratGaugeBalance)}`); + log(`gauge supply : ${formatUnits(data.gaugeSupply)}`); + log(`vault wS balance : ${formatUnits(data.vaultWSBalance)}`); + log(`strat wS balance : ${formatUnits(data.stratWSBalance)}`); +}; From a0ec2bd5614431203325db1519e9a10f6f5f6baf Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 16:01:43 +1100 Subject: [PATCH 65/80] More SwapX AMO fork testing --- .../sonic/swapx-amo.sonic.fork-test.js | 239 +++++------------- 1 file changed, 65 insertions(+), 174 deletions(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 7125f03d2c..1ab5398e09 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -92,38 +92,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Vault should deposit wS to AMO strategy", async function () { - const { swapXAMOStrategy, oSonic, swapXPool, oSonicVaultSigner, wS } = - fixture; - - const dataBefore = await snapData(); - await logSnapData(dataBefore); - - const wsDepositAmount = await parseUnits("5000"); - const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); - - // Vault transfers wS to strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, wsDepositAmount); - // Vault calls deposit on the strategy - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, wsDepositAmount); - - // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(wS.address, swapXPool.address, wsDepositAmount); - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(oSonic.address, swapXPool.address, osMintAmount); - - await assertChangedData(dataBefore, { - stratBalance: wsDepositAmount.add(osMintAmount), - osSupply: osMintAmount, - reserves: { ws: wsDepositAmount, os: osMintAmount }, - vaultWSBalance: wsDepositAmount.mul(-1), - }); + await assertDeposit(parseUnits("2000")); }); it("Only vault can deposit wS to AMO strategy", async function () { const { @@ -187,47 +156,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Vault should deposit wS to AMO strategy", async function () { - const { - clement, - swapXAMOStrategy, - oSonic, - oSonicVault, - swapXPool, - oSonicVaultSigner, - wS, - } = fixture; - - const wsDepositAmount = await parseUnits("5000"); - await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); - - const dataBefore = await snapData(); - await logSnapData(dataBefore); - - const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); - - // Vault transfers wS to strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, wsDepositAmount); - // Vault calls deposit on the strategy - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, wsDepositAmount); - - // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(wS.address, swapXPool.address, wsDepositAmount); - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(oSonic.address, swapXPool.address, osMintAmount); - - await assertChangedData(dataBefore, { - stratBalance: wsDepositAmount.add(osMintAmount), - osSupply: osMintAmount, - reserves: { ws: wsDepositAmount, os: osMintAmount }, - vaultWSBalance: wsDepositAmount.mul(-1), - }); + await assertDeposit(parseUnits("5000")); }); it("Vault should be able to withdraw all", async () => { const { swapXAMOStrategy, swapXPool, oSonic, oSonicVaultSigner, wS } = @@ -337,59 +266,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Vault should deposit wS to AMO strategy", async function () { - const { - clement, - swapXAMOStrategy, - oSonic, - oSonicVault, - swapXPool, - oSonicVaultSigner, - wS, - } = fixture; - - const wsDepositAmount = await parseUnits("5000"); - await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); - - const dataBefore = await snapData(); - await logSnapData(dataBefore, "Before depositing wS to strategy"); - - const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); - - // Vault transfers wS to strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, wsDepositAmount); - // Vault calls deposit on the strategy - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, wsDepositAmount); - - await logSnapData( - await snapData(), - `After depositing ${formatUnits(wsDepositAmount)} wS to strategy` - ); - - // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(wS.address, swapXPool.address, wsDepositAmount); - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(oSonic.address, swapXPool.address, osMintAmount); - - // Calculate the value of the wS and OS tokens added to the pool if the pool was balanced - const depositValue = calcReserveValue({ - ws: wsDepositAmount, - os: osMintAmount, - }); - log(`Value of deposit: ${formatUnits(depositValue)}`); - - await assertChangedData(dataBefore, { - stratBalance: depositValue, - osSupply: osMintAmount, - reserves: { ws: wsDepositAmount, os: osMintAmount }, - vaultWSBalance: wsDepositAmount.mul(-1), - }); + await assertDeposit(parseUnits("5000")); }); it("Strategist should swap a little assets to the pool", async () => { await assertSwapAssetsToPool(parseUnits("3")); @@ -441,6 +318,9 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { beforeEach(async () => { fixture = await loadFixture(); }); + it("Vault should deposit wS to AMO strategy", async function () { + await assertDeposit(parseUnits("12000")); + }); it("Strategist should swap a little assets to the pool", async () => { await assertSwapAssetsToPool(parseUnits("3")); }); @@ -489,54 +369,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { fixture = await loadFixture(); }); it("Vault should deposit wS to AMO strategy", async function () { - const { - clement, - swapXAMOStrategy, - oSonic, - oSonicVault, - swapXPool, - oSonicVaultSigner, - wS, - } = fixture; - - const wsDepositAmount = await parseUnits("5000"); - await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); - - const dataBefore = await snapData(); - await logSnapData(dataBefore); - - const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); - - // Vault transfers wS to strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, wsDepositAmount); - // Vault calls deposit on the strategy - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, wsDepositAmount); - - // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(wS.address, swapXPool.address, wsDepositAmount); - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(oSonic.address, swapXPool.address, osMintAmount); - - // Calculate the value of the wS and OS tokens added to the pool if the pool was balanced - const depositValue = calcReserveValue({ - ws: wsDepositAmount, - os: osMintAmount, - }); - log(`Value of deposit: ${formatUnits(depositValue)}`); - - await assertChangedData(dataBefore, { - stratBalance: depositValue, - osSupply: osMintAmount, - reserves: { ws: wsDepositAmount, os: osMintAmount }, - vaultWSBalance: wsDepositAmount.mul(-1), - }); + await assertDeposit(parseUnits("6000")); }); it("Strategist should add a little OS to the pool", async () => { const osAmount = parseUnits("0.3"); @@ -583,6 +416,9 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { beforeEach(async () => { fixture = await loadFixture(); }); + it("Vault should deposit wS to AMO strategy", async function () { + await assertDeposit(parseUnits("18000")); + }); it("Strategist should add a little OS to the pool", async () => { const osAmount = parseUnits("8"); await assertSwapOTokensToPool(osAmount, fixture); @@ -855,6 +691,61 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { } }; + async function assertDeposit(wsDepositAmount) { + const { + clement, + swapXAMOStrategy, + oSonic, + oSonicVault, + swapXPool, + oSonicVaultSigner, + wS, + } = fixture; + + await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); + + const dataBefore = await snapData(); + await logSnapData(dataBefore, "Before depositing wS to strategy"); + + const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); + + // Vault transfers wS to strategy + await wS + .connect(oSonicVaultSigner) + .transfer(swapXAMOStrategy.address, wsDepositAmount); + // Vault calls deposit on the strategy + const tx = await swapXAMOStrategy + .connect(oSonicVaultSigner) + .deposit(wS.address, wsDepositAmount); + + await logSnapData( + await snapData(), + `After depositing ${formatUnits(wsDepositAmount)} wS to strategy` + ); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(wS.address, swapXPool.address, wsDepositAmount); + await expect(tx) + .to.emit(swapXAMOStrategy, "Deposit") + .withArgs(oSonic.address, swapXPool.address, osMintAmount); + + // Calculate the value of the wS and OS tokens added to the pool if the pool was balanced + const depositValue = calcReserveValue({ + ws: wsDepositAmount, + os: osMintAmount, + }); + log(`Value of deposit: ${formatUnits(depositValue)}`); + + await assertChangedData(dataBefore, { + stratBalance: depositValue, + osSupply: osMintAmount, + reserves: { ws: wsDepositAmount, os: osMintAmount }, + vaultWSBalance: wsDepositAmount.mul(-1), + }); + } + async function assertSwapAssetsToPool(wsAmount) { const { swapXAMOStrategy, strategist } = fixture; From 0cb258b1c5fc3373da55e861c4e561d9020fd22b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 18:55:49 +1100 Subject: [PATCH 66/80] Changed _calcTokensToBurn in contract More fork tests --- .../sonic/SonicSwapXAMOStrategy.sol | 12 +- .../sonic/swapx-amo.sonic.fork-test.js | 309 ++++++++++++------ 2 files changed, 208 insertions(+), 113 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 79bb5c8690..38b7ab16ca 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -266,6 +266,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IVault(vaultAddress).burnForStrategy(osToBurn); // Transfer wS to the recipient + // Note there can be a dust amount of wS left in the strategy as + // the burn of the pool's LP tokens is rounded up IERC20(ws).safeTransfer(_recipient, _wsAmount); // Ensure solvency of the vault @@ -344,10 +346,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { _withdrawFromGaugeAndPool(lpTokens); // 2. Swap wS for OS against the pool - // Get the wS balance in the strategy as it could be less than the wsAmount due to rounding - uint256 wsInStrategy = IERC20(ws).balanceOf(address(this)); // Swap exact amount of wS for OS against the pool - _swapExactTokensForTokens(wsInStrategy, ws, os); + // There can be a dust amount of wS left in the strategy as the burn of the pool's LP tokens is rounded up + _swapExactTokensForTokens(_wsAmount, ws, os); // 3. Burn all the OS left in the strategy from the remove liquidity and swap uint256 osToBurn = IERC20(os).balanceOf(address(this)); @@ -356,7 +357,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Ensure solvency of the vault _solvencyAssert(); - emit SwapAssetsToPool(wsInStrategy, lpTokens, osToBurn); + emit SwapAssetsToPool(_wsAmount, lpTokens, osToBurn); } /** @@ -511,7 +512,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { (uint256 wsReserves, , ) = IPair(pool).getReserves(); require(wsReserves > 0, "Empty pool"); - lpTokens = ((_wsAmount + 1) * IPair(pool).totalSupply()) / wsReserves; + lpTokens = (_wsAmount * IPair(pool).totalSupply()) / wsReserves; + lpTokens += 1; // Add 1 to ensure we get enough LP tokens with rounding } /** diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 1ab5398e09..b51e55c222 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -159,74 +159,10 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await assertDeposit(parseUnits("5000")); }); it("Vault should be able to withdraw all", async () => { - const { swapXAMOStrategy, swapXPool, oSonic, oSonicVaultSigner, wS } = - fixture; - - const dataBefore = await snapData(); - await logSnapData(dataBefore); - - const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts(); - - // Now try to withdraw all the wS from the strategy - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdrawAll(); - - await logSnapData(await snapData()); - - // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(wS.address, swapXPool.address, wsWithdrawAmount); - await expect(tx) - .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(oSonic.address, swapXPool.address, osBurnAmount); - - await assertChangedData(dataBefore, { - stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), - osSupply: osBurnAmount.mul(-1), - reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, - vaultWSBalance: wsWithdrawAmount, - }); + await assertWithdrawAll(); }); it("Vault should be able to partially withdraw", async () => { - const { - swapXAMOStrategy, - oSonic, - swapXPool, - oSonicVault, - oSonicVaultSigner, - wS, - } = fixture; - - const dataBefore = await snapData(); - - const wsWithdrawAmount = parseUnits("1000"); - const osBurnAmount = await calcOSWithdrawAmount(wsWithdrawAmount); - - // Now try to withdraw the wS from the strategy - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdraw(oSonicVault.address, wS.address, wsWithdrawAmount); - - // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(wS.address, swapXPool.address, wsWithdrawAmount); - await expect(tx).to.emit(swapXAMOStrategy, "Withdrawal").withNamedArgs({ - _asset: oSonic.address, - _pToken: swapXPool.address, - }); - - await assertChangedData(dataBefore, { - stratBalance: wsWithdrawAmount.add(osBurnAmount).mul(-1), - osSupply: osBurnAmount.mul(-1), - reserves: { - ws: wsWithdrawAmount.mul(-1), - os: osBurnAmount.mul(-1), - }, - vaultWSBalance: wsWithdrawAmount, - }); + await assertWithdrawPartial(parseUnits("1000")); }); it("Only vault can withdraw wS from AMO strategy", async function () { const { swapXAMOStrategy, oSonicVault, strategist, timelock, nick, wS } = @@ -268,6 +204,12 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { it("Vault should deposit wS to AMO strategy", async function () { await assertDeposit(parseUnits("5000")); }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("1000")); + }); it("Strategist should swap a little assets to the pool", async () => { await assertSwapAssetsToPool(parseUnits("3")); }); @@ -321,6 +263,12 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { it("Vault should deposit wS to AMO strategy", async function () { await assertDeposit(parseUnits("12000")); }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("1000")); + }); it("Strategist should swap a little assets to the pool", async () => { await assertSwapAssetsToPool(parseUnits("3")); }); @@ -371,6 +319,12 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { it("Vault should deposit wS to AMO strategy", async function () { await assertDeposit(parseUnits("6000")); }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("1000")); + }); it("Strategist should add a little OS to the pool", async () => { const osAmount = parseUnits("0.3"); await assertSwapOTokensToPool(osAmount, fixture); @@ -419,6 +373,12 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { it("Vault should deposit wS to AMO strategy", async function () { await assertDeposit(parseUnits("18000")); }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("1000")); + }); it("Strategist should add a little OS to the pool", async () => { const osAmount = parseUnits("8"); await assertSwapOTokensToPool(osAmount, fixture); @@ -575,18 +535,45 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { return k; }; + // Babylonian square root function for Ethers.js BigNumber function sqrt(value) { - const ONE = ethers.BigNumber.from(1); - const TWO = ethers.BigNumber.from(2); - - const x = ethers.BigNumber.from(value); - let z = x.add(ONE).div(TWO); - let y = x; - while (z.sub(y).isNegative()) { - y = z; - z = x.div(z).add(z).div(TWO); + // Convert input to BigNumber if it isn't already + let bn = ethers.BigNumber.from(value); + + // Handle edge cases + if (bn.lt(0)) { + throw new Error("Square root of negative number is not supported"); + } + if (bn.eq(0)) { + return ethers.BigNumber.from(0); + } + + // Initial guess (number / 2) + let guess = bn.div(2); + + // Define precision threshold (in wei scale, 10^-18) + const epsilon = ethers.BigNumber.from("1"); // 1 wei precision + + // Keep refining until we reach desired precision + while (true) { + // Babylonian method: nextGuess = (guess + number/guess) / 2 + // Using mul and div for BigNumber arithmetic + let numerator = guess.add(bn.div(guess)); + let nextGuess = numerator.div(2); + + // Calculate absolute difference + let diff = nextGuess.gt(guess) + ? nextGuess.sub(guess) + : guess.sub(nextGuess); + + // If difference is less than epsilon, we're done + if (diff.lte(epsilon)) { + return nextGuess; + } + + // Update guess for next iteration + guess = nextGuess; } - return y; } const snapData = async () => { @@ -625,24 +612,26 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const expectedStratBalance = dataBefore.stratBalance.add( delta.stratBalance ); - // Truncate any dust amounts to zero - const expectedStratBalanceTruncated = expectedStratBalance - .div(100000) - .mul(100000); - expect( - await swapXAMOStrategy.checkBalance(wS.address) - ).to.approxEqualTolerance( - expectedStratBalanceTruncated, - "0.01", // 0.01% (1 bps) tolerance + log(`Expected strategy balance: ${formatUnits(expectedStratBalance)}`); + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( + expectedStratBalance.sub(3), + expectedStratBalance.add(3), "Strategy's check balance" ); } if (delta.osSupply != undefined) { const expectedSupply = dataBefore.osSupply.add(delta.osSupply); - expect(await oSonic.totalSupply()).to.equal( - expectedSupply, - "OSonic total supply" + expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( + expectedSupply + ); + } + + // Check Vault's wS balance + if (delta.vaultWSBalance != undefined) { + expect(await wS.balanceOf(oSonicVault.address)).to.equal( + dataBefore.vaultWSBalance.add(delta.vaultWSBalance), + "Vault's wS balance" ); } @@ -650,13 +639,21 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { if (delta.reserves != undefined) { const { _reserve0: wsReserves, _reserve1: osReserves } = await swapXPool.getReserves(); - expect(wsReserves).to.equal( - dataBefore.reserves.ws.add(delta.reserves.ws), - "wS reserves" - ); - expect(osReserves).to.equal( - dataBefore.reserves.os.add(delta.reserves.os), - "OS reserves" + + // If the wS reserves delta is a function, call it to check the wS reserves + if (typeof delta.reserves.ws == "function") { + delta.reserves.ws(wsReserves); + + // Exit early as we can't calculate the gauge balance + return; + } else { + expect(wsReserves, "wS reserves").to.equal( + dataBefore.reserves.ws.add(delta.reserves.ws) + ); + } + // Check OS reserves delta + expect(osReserves, "OS reserves").to.equal( + dataBefore.reserves.os.add(delta.reserves.os) ); // Check the strategy's gauge balance @@ -681,14 +678,6 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { "Strategy's gauge balance" ); } - - // Check Vault's wS balance - if (delta.vaultWSBalance != undefined) { - expect(await wS.balanceOf(oSonicVault.address)).to.equal( - dataBefore.vaultWSBalance.add(delta.vaultWSBalance), - "Vault's wS balance" - ); - } }; async function assertDeposit(wsDepositAmount) { @@ -736,7 +725,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { ws: wsDepositAmount, os: osMintAmount, }); - log(`Value of deposit: ${formatUnits(depositValue)}`); + // log(`Value of deposit: ${formatUnits(depositValue)}`); await assertChangedData(dataBefore, { stratBalance: depositValue, @@ -746,6 +735,96 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); } + async function assertWithdrawAll() { + const { swapXAMOStrategy, swapXPool, oSonic, oSonicVaultSigner, wS } = + fixture; + + const dataBefore = await snapData(); + await logSnapData(dataBefore); + + const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts(); + + // Now try to withdraw all the wS from the strategy + const tx = await swapXAMOStrategy.connect(oSonicVaultSigner).withdrawAll(); + + await logSnapData(await snapData()); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Withdrawal") + .withArgs(wS.address, swapXPool.address, wsWithdrawAmount); + await expect(tx) + .to.emit(swapXAMOStrategy, "Withdrawal") + .withArgs(oSonic.address, swapXPool.address, osBurnAmount); + + // Calculate the value of the wS and OS tokens removed from the pool if the pool was balanced + const withdrawValue = calcReserveValue({ + ws: wsWithdrawAmount, + os: osBurnAmount, + }); + + await assertChangedData(dataBefore, { + stratBalance: withdrawValue.mul(-1), + osSupply: osBurnAmount.mul(-1), + reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, + vaultWSBalance: wsWithdrawAmount, + }); + } + + async function assertWithdrawPartial(wsWithdrawAmount) { + const { + swapXAMOStrategy, + oSonic, + swapXPool, + oSonicVault, + oSonicVaultSigner, + wS, + } = fixture; + + const dataBefore = await snapData(); + + const osBurnAmount = await calcOSWithdrawAmount(wsWithdrawAmount); + + // Now try to withdraw the wS from the strategy + const tx = await swapXAMOStrategy + .connect(oSonicVaultSigner) + .withdraw(oSonicVault.address, wS.address, wsWithdrawAmount); + + // Check emitted events + await expect(tx) + .to.emit(swapXAMOStrategy, "Withdrawal") + .withArgs(wS.address, swapXPool.address, wsWithdrawAmount); + await expect(tx).to.emit(swapXAMOStrategy, "Withdrawal").withNamedArgs({ + _asset: oSonic.address, + _pToken: swapXPool.address, + }); + + // Calculate the value of the wS and OS tokens removed from the pool if the pool was balanced + const withdrawValue = calcReserveValue({ + ws: wsWithdrawAmount, + os: osBurnAmount, + }); + + await assertChangedData(dataBefore, { + stratBalance: withdrawValue.mul(-1), + osSupply: osBurnAmount.mul(-1), + reserves: { + ws: (actualWsReserve) => { + const expectedWsReserves = + dataBefore.reserves.ws.sub(wsWithdrawAmount); + + expect(actualWsReserve).to.withinRange( + expectedWsReserves.sub(2), + expectedWsReserves, + "wS reserves" + ); + }, + os: osBurnAmount.mul(-1), + }, + vaultWSBalance: wsWithdrawAmount, + }); + } + async function assertSwapAssetsToPool(wsAmount) { const { swapXAMOStrategy, strategist } = fixture; @@ -758,7 +837,13 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { // Check emitted event await expect(tx).to.emittedEvent("SwapAssetsToPool", [ - wsAmount, + (actualWsAmount) => { + expect(actualWsAmount).to.withinRange( + wsAmount.sub(1), + wsAmount.add(1), + "SwapAssetsToPool event wsAmount" + ); + }, (lpTokens) => { // TODO better calculate the value of LP tokens expect(lpTokens).to.approxEqualTolerance( @@ -773,9 +858,11 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }, ]); + // TODO check the change in the strategy's balance await assertChangedData( dataBefore, { + // stratBalance: 0, vaultWSBalance: 0, }, fixture @@ -811,15 +898,21 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { } // Calculate the amount of OS burnt from a withdraw - async function calcOSWithdrawAmount(wethWithdrawAmount) { + async function calcOSWithdrawAmount(wsWithdrawAmount) { const { swapXPool } = fixture; // Get the reserves of the pool const { _reserve0: wsReserves, _reserve1: osReserves } = await swapXPool.getReserves(); - // OS to burn = wS withdrawn * OS reserves / wS reserves - const osBurnAmount = wethWithdrawAmount.mul(osReserves).div(wsReserves); + // lp tokens to burn = wS withdrawn * total LP supply / wS pool balance + const totalLpSupply = await swapXPool.totalSupply(); + const lpBurnAmount = wsWithdrawAmount + .mul(totalLpSupply) + .div(wsReserves) + .add(1); + // OS to burn = LP tokens to burn * OS reserves / total LP supply + const osBurnAmount = lpBurnAmount.mul(osReserves).div(totalLpSupply); log(`OS burn amount : ${formatUnits(osBurnAmount)}`); From e081a439bacde650a383e4004ae8d404279a0d84 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 18:56:22 +1100 Subject: [PATCH 67/80] Removed unnecessary line in withinRange matcher --- contracts/test/helpers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index 59125f272b..404027db79 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -12,7 +12,6 @@ const { decimalsFor, units } = require("../utils/units"); */ chai.Assertion.addMethod("withinRange", function (min, max, message) { const actual = this._obj; - min = BigNumber.from(min); chai.expect(actual, message).gte(min); chai.expect(actual, message).lte(max); From ee34c7f7b9a5d63ff3a8d1d583d7f029c1a28585 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 19:01:54 +1100 Subject: [PATCH 68/80] Removed use of safeTransfer --- .../strategies/sonic/SonicSwapXAMOStrategy.sol | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 38b7ab16ca..56a652eeb7 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -7,7 +7,6 @@ pragma solidity ^0.8.0; * @author Origin Protocol Inc */ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { StableMath } from "../../utils/StableMath.sol"; @@ -18,7 +17,6 @@ import { IGauge } from "../../interfaces/sonic/ISwapXGauge.sol"; import { IVault } from "../../interfaces/IVault.sol"; contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { - using SafeERC20 for IERC20; using StableMath for uint256; using SafeCast for uint256; @@ -268,7 +266,11 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // Transfer wS to the recipient // Note there can be a dust amount of wS left in the strategy as // the burn of the pool's LP tokens is rounded up - IERC20(ws).safeTransfer(_recipient, _wsAmount); + require( + IERC20(ws).balanceOf(address(this)) >= _wsAmount, + "Not enough wS removed from pool" + ); + IERC20(ws).transfer(_recipient, _wsAmount); // Ensure solvency of the vault _solvencyAssert(); @@ -310,7 +312,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // This includes all that was removed from the SwapX pool and // any that was sitting in the strategy contract before the removal. uint256 wsBalance = IERC20(ws).balanceOf(address(this)); - IERC20(ws).safeTransfer(vaultAddress, wsBalance); + IERC20(ws).transfer(vaultAddress, wsBalance); // Emit event for the withdrawn wS tokens emit Withdrawal(ws, pool, wsBalance); @@ -528,9 +530,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { returns (uint256 lpTokens) { // Transfer wS to the pool - IERC20(ws).safeTransfer(pool, _wsAmount); + IERC20(ws).transfer(pool, _wsAmount); // Transfer OS to the pool - IERC20(os).safeTransfer(pool, osAmount); + IERC20(os).transfer(pool, osAmount); // Mint LP tokens from the pool lpTokens = IPair(pool).mint(address(this)); @@ -553,7 +555,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IGauge(gauge).withdraw(lpTokens); // Transfer the pool LP tokens to the pool - IERC20(pool).safeTransfer(pool, lpTokens); + IERC20(pool).transfer(pool, lpTokens); // Burn the LP tokens and transfer the wS and OS back to the strategy IPair(pool).burn(address(this)); @@ -571,7 +573,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { address _tokenOut ) internal { // Transfer in tokens to the pool - IERC20(_tokenIn).safeTransfer(pool, _amountIn); + IERC20(_tokenIn).transfer(pool, _amountIn); // Calculate how much out tokens we get from the swap uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); From 1813846f3e79b851490b8b48ae71e113a8ac77fe Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 19:45:43 +1100 Subject: [PATCH 69/80] More SwapX AMO fork tests --- .../sonic/swapx-amo.sonic.fork-test.js | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index b51e55c222..2a4c1ff3b5 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -208,7 +208,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await assertWithdrawAll(); }); it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); + await assertWithdrawPartial(parseUnits("4000")); }); it("Strategist should swap a little assets to the pool", async () => { await assertSwapAssetsToPool(parseUnits("3")); @@ -642,10 +642,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { // If the wS reserves delta is a function, call it to check the wS reserves if (typeof delta.reserves.ws == "function") { + // Call test function to check the wS reserves delta.reserves.ws(wsReserves); - - // Exit early as we can't calculate the gauge balance - return; } else { expect(wsReserves, "wS reserves").to.equal( dataBefore.reserves.ws.add(delta.reserves.ws) @@ -655,20 +653,12 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { expect(osReserves, "OS reserves").to.equal( dataBefore.reserves.os.add(delta.reserves.os) ); + } + if (delta.stratGaugeBalance) { // Check the strategy's gauge balance - // Calculate the liquidity added to the pool - const wsLiquidity = delta.reserves.ws - .mul(dataBefore.poolSupply) - .div(dataBefore.reserves.ws); - const osLiquidity = delta.reserves.os - .mul(dataBefore.poolSupply) - .div(dataBefore.reserves.os); - const deltaStratGaugeBalance = wsLiquidity.lt(osLiquidity) - ? wsLiquidity - : osLiquidity; const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( - deltaStratGaugeBalance + delta.stratGaugeBalance ); expect( await swapXGauge.balanceOf(swapXAMOStrategy.address) @@ -696,7 +686,9 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const dataBefore = await snapData(); await logSnapData(dataBefore, "Before depositing wS to strategy"); - const osMintAmount = await calcOSMintAmount(fixture, wsDepositAmount); + const { lpMintAmount, osMintAmount } = await calcOSMintAmount( + wsDepositAmount + ); // Vault transfers wS to strategy await wS @@ -732,6 +724,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { osSupply: osMintAmount, reserves: { ws: wsDepositAmount, os: osMintAmount }, vaultWSBalance: wsDepositAmount.mul(-1), + gaugeSupply: lpMintAmount, }); } @@ -768,6 +761,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { osSupply: osBurnAmount.mul(-1), reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, vaultWSBalance: wsWithdrawAmount, + stratGaugeBalance: dataBefore.gaugeSupply.mul(-1), }); } @@ -783,7 +777,9 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const dataBefore = await snapData(); - const osBurnAmount = await calcOSWithdrawAmount(wsWithdrawAmount); + const { lpBurnAmount, osBurnAmount } = await calcOSWithdrawAmount( + wsWithdrawAmount + ); // Now try to withdraw the wS from the strategy const tx = await swapXAMOStrategy @@ -822,6 +818,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { os: osBurnAmount.mul(-1), }, vaultWSBalance: wsWithdrawAmount, + gaugeSupply: lpBurnAmount.mul(-1), }); } @@ -864,6 +861,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { { // stratBalance: 0, vaultWSBalance: 0, + stratGaugeBalance: 0, }, fixture ); @@ -884,7 +882,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { } // Calculate the minted OS amount for a deposit - async function calcOSMintAmount(fixture, wsDepositAmount) { + async function calcOSMintAmount(wsDepositAmount) { const { swapXPool } = fixture; // Get the reserves of the pool @@ -894,7 +892,10 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const osMintAmount = wsDepositAmount.mul(osReserves).div(wsReserves); log(`OS mint amount : ${formatUnits(osMintAmount)}`); - return osMintAmount; + const lpTotalSupply = await swapXPool.totalSupply(); + const lpMintAmount = wsDepositAmount.mul(lpTotalSupply).div(wsReserves); + + return { lpMintAmount, osMintAmount }; } // Calculate the amount of OS burnt from a withdraw @@ -916,7 +917,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { log(`OS burn amount : ${formatUnits(osBurnAmount)}`); - return osBurnAmount; + return { lpBurnAmount, osBurnAmount }; } // Calculate the OS and wS amounts from a withdrawAll From 17dc9265a1e441132df87d87717ef4e09a9964b7 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 20:59:39 +1100 Subject: [PATCH 70/80] More SwapX AMO fork tests --- .../sonic/swapx-amo.sonic.fork-test.js | 152 ++++++++++++------ 1 file changed, 103 insertions(+), 49 deletions(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 2a4c1ff3b5..c73df46d35 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -325,11 +325,11 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { it("Vault should be able to partially withdraw", async () => { await assertWithdrawPartial(parseUnits("1000")); }); - it("Strategist should add a little OS to the pool", async () => { + it("Strategist should swap a little OS to the pool", async () => { const osAmount = parseUnits("0.3"); await assertSwapOTokensToPool(osAmount, fixture); }); - it("Strategist should add a lot of OS to the pool", async () => { + it("Strategist should swap a lot of OS to the pool", async () => { const osAmount = parseUnits("5000"); await assertSwapOTokensToPool(osAmount, fixture); }); @@ -379,7 +379,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { it("Vault should be able to partially withdraw", async () => { await assertWithdrawPartial(parseUnits("1000")); }); - it("Strategist should add a little OS to the pool", async () => { + it("Strategist should swap a little OS to the pool", async () => { const osAmount = parseUnits("8"); await assertSwapOTokensToPool(osAmount, fixture); }); @@ -474,7 +474,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { // Swap wS into the pool and OS out await poolSwapTokensIn(wS, parseUnits("1006000")); - await logSnapData(await snapData(), "After swapping wS into the pool"); + await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); // Assert the strategy's balance expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( @@ -582,6 +582,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const stratBalance = await swapXAMOStrategy.checkBalance(wS.address); const osSupply = await oSonic.totalSupply(); + const vaultAssets = await oSonicVault.totalValue(); const poolSupply = await swapXPool.totalSupply(); const { _reserve0: wsReserves, _reserve1: osReserves } = await swapXPool.getReserves(); @@ -595,6 +596,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { return { stratBalance, osSupply, + vaultAssets, poolSupply, reserves: { ws: wsReserves, os: osReserves }, stratGaugeBalance, @@ -604,6 +606,60 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }; }; + const logSnapData = async (data, message) => { + const totalReserves = data.reserves.ws.add(data.reserves.os); + const reserversPercentage = { + ws: data.reserves.ws.mul(10000).div(totalReserves), + os: data.reserves.os.mul(10000).div(totalReserves), + }; + if (message) { + log(message); + } + log(`Strategy balance : ${formatUnits(data.stratBalance)}`); + log(`OS supply : ${formatUnits(data.osSupply)}`); + log(`Vault assets : ${formatUnits(data.vaultAssets)}`); + log(`pool supply : ${formatUnits(data.poolSupply)}`); + log( + `reserves wS : ${formatUnits(data.reserves.ws)} ${formatUnits( + reserversPercentage.ws, + 2 + )}%` + ); + log( + `reserves OS : ${formatUnits(data.reserves.os)} ${formatUnits( + reserversPercentage.os, + 2 + )}%` + ); + log(`strat gauge balance : ${formatUnits(data.stratGaugeBalance)}`); + log(`gauge supply : ${formatUnits(data.gaugeSupply)}`); + log(`vault wS balance : ${formatUnits(data.vaultWSBalance)}`); + log(`strat wS balance : ${formatUnits(data.stratWSBalance)}`); + }; + + const logProfit = async (dataBefore) => { + const { oSonic, oSonicVault } = fixture; + + const osSupplyAfter = await oSonic.totalSupply(); + const vaultAssetsAfter = await oSonicVault.totalValue(); + const profit = vaultAssetsAfter + .sub(dataBefore.vaultAssets) + .add(dataBefore.osSupply.sub(osSupplyAfter)); + log( + `Change vault assets : ${formatUnits( + vaultAssetsAfter.sub(dataBefore.vaultAssets) + )}` + ); + log( + `Change OS supply : ${formatUnits( + dataBefore.osSupply.sub(osSupplyAfter) + )}` + ); + log(`Profit : ${formatUnits(profit)}`); + + return profit; + }; + const assertChangedData = async (dataBefore, delta) => { const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = fixture; @@ -684,7 +740,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); const dataBefore = await snapData(); - await logSnapData(dataBefore, "Before depositing wS to strategy"); + await logSnapData(dataBefore, "\nBefore depositing wS to strategy"); const { lpMintAmount, osMintAmount } = await calcOSMintAmount( wsDepositAmount @@ -701,8 +757,9 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await logSnapData( await snapData(), - `After depositing ${formatUnits(wsDepositAmount)} wS to strategy` + `\nAfter depositing ${formatUnits(wsDepositAmount)} wS to strategy` ); + await logProfit(dataBefore); // Check emitted events await expect(tx) @@ -740,7 +797,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { // Now try to withdraw all the wS from the strategy const tx = await swapXAMOStrategy.connect(oSonicVaultSigner).withdrawAll(); - await logSnapData(await snapData()); + await logSnapData(await snapData(), "\nAfter full withdraw"); + await logProfit(dataBefore); // Check emitted events await expect(tx) @@ -786,6 +844,12 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { .connect(oSonicVaultSigner) .withdraw(oSonicVault.address, wS.address, wsWithdrawAmount); + await logSnapData( + await snapData(), + `\nAfter withdraw of ${formatUnits(wsWithdrawAmount)}` + ); + await logProfit(dataBefore); + // Check emitted events await expect(tx) .to.emit(swapXAMOStrategy, "Withdrawal") @@ -823,15 +887,25 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { } async function assertSwapAssetsToPool(wsAmount) { - const { swapXAMOStrategy, strategist } = fixture; + const { swapXAMOStrategy, swapXPool, strategist, wS } = fixture; const dataBefore = await snapData(); + await logSnapData(dataBefore, "Before swapping assets to the pool"); + + const { lpBurnAmount: expectedLpBurnAmount, osBurnAmount: osBurnAmount1 } = + await calcOSWithdrawAmount(wsAmount); + // TODO this is not accurate as the liquidity needs to be removed first + const osBurnAmount2 = await swapXPool.getAmountOut(wsAmount, wS.address); + const osBurnAmount = osBurnAmount1.add(osBurnAmount2); // Swap wS to the pool and burn the received OS from the pool const tx = await swapXAMOStrategy .connect(strategist) .swapAssetsToPool(wsAmount); + await logSnapData(await snapData(), "\nAfter swapping assets to the pool"); + await logProfit(dataBefore); + // Check emitted event await expect(tx).to.emittedEvent("SwapAssetsToPool", [ (actualWsAmount) => { @@ -841,25 +915,21 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { "SwapAssetsToPool event wsAmount" ); }, - (lpTokens) => { - // TODO better calculate the value of LP tokens - expect(lpTokens).to.approxEqualTolerance( - wsAmount, + expectedLpBurnAmount, + (actualOsBurnAmount) => { + // TODO this can be tightened once osBurnAmount is more accurately calculated + expect(actualOsBurnAmount).to.approxEqualTolerance( + osBurnAmount, 10, - "SwapAssetsToPool lpTokens" + "SwapAssetsToPool event osBurnt" ); }, - (osBurnt) => { - // TODO narrow down the range - expect(osBurnt).to.gt(wsAmount, 1); - }, ]); - // TODO check the change in the strategy's balance await assertChangedData( dataBefore, { - // stratBalance: 0, + // stratBalance: osBurnAmount.mul(-1), vaultWSBalance: 0, stratGaugeBalance: 0, }, @@ -870,6 +940,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { async function assertSwapOTokensToPool(osAmount) { const { swapXAMOStrategy, strategist } = fixture; + const dataBefore = await snapData(); + // Mint OS and swap into the pool, then mint more OS to add with the wS swapped out const tx = await swapXAMOStrategy .connect(strategist) @@ -879,6 +951,18 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await expect(tx) .emit(swapXAMOStrategy, "SwapOTokensToPool") .withNamedArgs({ osMinted: osAmount }); + + await logProfit(dataBefore); + + await assertChangedData( + dataBefore, + { + // stratBalance: osBurnAmount.mul(-1), + vaultWSBalance: 0, + stratGaugeBalance: 0, + }, + fixture + ); } // Calculate the minted OS amount for a deposit @@ -948,33 +1032,3 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }; } }); - -const logSnapData = async (data, message) => { - const totalReserves = data.reserves.ws.add(data.reserves.os); - const reserversPercentage = { - ws: data.reserves.ws.mul(10000).div(totalReserves), - os: data.reserves.os.mul(10000).div(totalReserves), - }; - if (message) { - log(message); - } - log(`Strategy balance : ${formatUnits(data.stratBalance)}`); - log(`OS supply : ${formatUnits(data.osSupply)}`); - log(`pool supply : ${formatUnits(data.poolSupply)}`); - log( - `reserves wS : ${formatUnits(data.reserves.ws)} ${formatUnits( - reserversPercentage.ws, - 2 - )}%` - ); - log( - `reserves OS : ${formatUnits(data.reserves.os)} ${formatUnits( - reserversPercentage.os, - 2 - )}%` - ); - log(`strat gauge balance : ${formatUnits(data.stratGaugeBalance)}`); - log(`gauge supply : ${formatUnits(data.gaugeSupply)}`); - log(`vault wS balance : ${formatUnits(data.vaultWSBalance)}`); - log(`strat wS balance : ${formatUnits(data.stratWSBalance)}`); -}; From e57f13ee5a272e710d1d2f55117740e46aadf14e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 21:39:46 +1100 Subject: [PATCH 71/80] Added SwapX AMO Strategy contracts to diagrams --- .../contracts/strategies/sonic/README.md | 28 ++++ .../docs/SonicCurveAMOStrategyHierarchy.svg | 74 ++++++++++ .../docs/SonicCurveAMOStrategySquashed.svg | 119 ++++++++++++++++ .../docs/SonicCurveAMOStrategyStorage.svg | 129 ++++++++++++++++++ .../docs/SonicSwapXAMOStrategyHierarchy.svg | 61 +++++++++ .../docs/SonicSwapXAMOStrategySquashed.svg | 117 ++++++++++++++++ .../docs/SonicSwapXAMOStrategyStorage.svg | 125 +++++++++++++++++ contracts/docs/generate.sh | 8 ++ contracts/docs/plantuml/sonicContracts.png | Bin 53105 -> 60950 bytes contracts/docs/plantuml/sonicContracts.puml | 19 ++- 10 files changed, 679 insertions(+), 1 deletion(-) create mode 100644 contracts/docs/SonicCurveAMOStrategyHierarchy.svg create mode 100644 contracts/docs/SonicCurveAMOStrategySquashed.svg create mode 100644 contracts/docs/SonicCurveAMOStrategyStorage.svg create mode 100644 contracts/docs/SonicSwapXAMOStrategyHierarchy.svg create mode 100644 contracts/docs/SonicSwapXAMOStrategySquashed.svg create mode 100644 contracts/docs/SonicSwapXAMOStrategyStorage.svg diff --git a/contracts/contracts/strategies/sonic/README.md b/contracts/contracts/strategies/sonic/README.md index 87e25ccdc2..3cd7f3b743 100644 --- a/contracts/contracts/strategies/sonic/README.md +++ b/contracts/contracts/strategies/sonic/README.md @@ -13,3 +13,31 @@ ### Storage ![Sonic Staking Strategy Storage](../../../docs/SonicStakingStrategyStorage.svg) + +## Curve AMO Strategy + +### Hierarchy + +![Curve AMO Strategy Hierarchy](../../../docs/SonicCurveAMOStrategyHierarchy.svg) + +### Squashed + +![Curve AMO Strategy Squashed](../../../docs/SonicCurveAMOStrategySquashed.svg) + +### Storage + +![Curve AMO Strategy Storage](../../../docs/SonicCurveAMOStrategyStorage.svg) + +## SwapX AMO Strategy + +### Hierarchy + +![SwapX AMO Strategy Hierarchy](../../../docs/SonicSwapXAMOStrategyHierarchy.svg) + +### Squashed + +![SwapX AMO Strategy Squashed](../../../docs/SonicSwapXAMOStrategySquashed.svg) + +### Storage + +![SwapX AMO Strategy Storage](../../../docs/SonicSwapXAMOStrategyStorage.svg) diff --git a/contracts/docs/SonicCurveAMOStrategyHierarchy.svg b/contracts/docs/SonicCurveAMOStrategyHierarchy.svg new file mode 100644 index 0000000000..d2c3db1806 --- /dev/null +++ b/contracts/docs/SonicCurveAMOStrategyHierarchy.svg @@ -0,0 +1,74 @@ + + + + + + +UmlClassDiagram + + + +21 + +Governable +../contracts/governance/Governable.sol + + + +229 + +BaseCurveAMOStrategy +../contracts/strategies/BaseCurveAMOStrategy.sol + + + +290 + +<<Abstract>> +InitializableAbstractStrategy +../contracts/utils/InitializableAbstractStrategy.sol + + + +229->290 + + + + + +410 + +SonicCurveAMOStrategy +../contracts/strategies/sonic/SonicCurveAMOStrategy.sol + + + +410->229 + + + + + +289 + +<<Abstract>> +Initializable +../contracts/utils/Initializable.sol + + + +290->21 + + + + + +290->289 + + + + + diff --git a/contracts/docs/SonicCurveAMOStrategySquashed.svg b/contracts/docs/SonicCurveAMOStrategySquashed.svg new file mode 100644 index 0000000000..bf0f450ae2 --- /dev/null +++ b/contracts/docs/SonicCurveAMOStrategySquashed.svg @@ -0,0 +1,119 @@ + + + + + + +UmlClassDiagram + + + +410 + +SonicCurveAMOStrategy +../contracts/strategies/sonic/SonicCurveAMOStrategy.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _deprecated_platformAddress: address <<InitializableAbstractStrategy>> +   _deprecated_vaultAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardTokenAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardLiquidationThreshold: uint256 <<InitializableAbstractStrategy>> +   _reserved: int256[98] <<InitializableAbstractStrategy>> +Internal: +   assetsMapped: address[] <<InitializableAbstractStrategy>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   platformAddress: address <<InitializableAbstractStrategy>> +   vaultAddress: address <<InitializableAbstractStrategy>> +   assetToPToken: mapping(address=>address) <<InitializableAbstractStrategy>> +   harvesterAddress: address <<InitializableAbstractStrategy>> +   rewardTokenAddresses: address[] <<InitializableAbstractStrategy>> +   SOLVENCY_THRESHOLD: uint256 <<BaseCurveAMOStrategy>> +   weth: IWETH9 <<BaseCurveAMOStrategy>> +   oeth: IERC20 <<BaseCurveAMOStrategy>> +   lpToken: IERC20 <<BaseCurveAMOStrategy>> +   curvePool: ICurveStableSwapNG <<BaseCurveAMOStrategy>> +   gauge: ICurveXChainLiquidityGauge <<BaseCurveAMOStrategy>> +   gaugeFactory: IChildLiquidityGaugeFactory <<BaseCurveAMOStrategy>> +   oethCoinIndex: uint128 <<BaseCurveAMOStrategy>> +   wethCoinIndex: uint128 <<BaseCurveAMOStrategy>> +   maxSlippage: uint256 <<BaseCurveAMOStrategy>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _initialize(_rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<InitializableAbstractStrategy>> +    _collectRewardTokens() <<InitializableAbstractStrategy>> +    _setPTokenAddress(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    _abstractSetPToken(_asset: address, _pToken: address) <<BaseCurveAMOStrategy>> +    _deposit(_weth: address, _wethAmount: uint256) <<BaseCurveAMOStrategy>> +    calcTokenToBurn(_wethAmount: uint256): (lpToBurn: uint256) <<BaseCurveAMOStrategy>> +    _withdrawAndRemoveFromPool(_lpTokens: uint256, coinIndex: uint128): (coinsRemoved: uint256) <<BaseCurveAMOStrategy>> +    _solvencyAssert() <<BaseCurveAMOStrategy>> +    _lpWithdraw(_lpAmount: uint256) <<BaseCurveAMOStrategy>> +    _setMaxSlippage(_maxSlippage: uint256) <<BaseCurveAMOStrategy>> +    _approveBase() <<BaseCurveAMOStrategy>> +    _max(a: int256, b: int256): int256 <<BaseCurveAMOStrategy>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    collectRewardTokens() <<onlyHarvester, nonReentrant>> <<BaseCurveAMOStrategy>> +    setRewardTokenAddresses(_rewardTokenAddresses: address[]) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    getRewardTokenAddresses(): address[] <<InitializableAbstractStrategy>> +    setPTokenAddress(_asset: address, _pToken: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    removePToken(_assetIndex: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    setHarvesterAddress(_harvesterAddress: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    safeApproveAllTokens() <<onlyGovernor, nonReentrant>> <<BaseCurveAMOStrategy>> +    deposit(_weth: address, _amount: uint256) <<onlyVault, nonReentrant>> <<BaseCurveAMOStrategy>> +    depositAll() <<onlyVault, nonReentrant>> <<BaseCurveAMOStrategy>> +    withdraw(_recipient: address, _weth: address, _amount: uint256) <<onlyVault, nonReentrant>> <<BaseCurveAMOStrategy>> +    withdrawAll() <<onlyVaultOrGovernor, nonReentrant>> <<BaseCurveAMOStrategy>> +    checkBalance(_asset: address): (balance: uint256) <<BaseCurveAMOStrategy>> +    initialize(_rewardTokenAddresses: address[], _maxSlippage: uint256) <<onlyGovernor, initializer>> <<BaseCurveAMOStrategy>> +    mintAndAddOTokens(_oTokens: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance>> <<BaseCurveAMOStrategy>> +    removeAndBurnOTokens(_lpTokens: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance>> <<BaseCurveAMOStrategy>> +    removeOnlyAssets(_lpTokens: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance>> <<BaseCurveAMOStrategy>> +    setMaxSlippage(_maxSlippage: uint256) <<onlyGovernor>> <<BaseCurveAMOStrategy>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> PTokenAdded(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> PTokenRemoved(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> Deposit(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> Withdrawal(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenCollected(recipient: address, rewardToken: address, amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenAddressesUpdated(_oldAddresses: address[], _newAddresses: address[]) <<InitializableAbstractStrategy>> +    <<event>> HarvesterAddressesUpdated(_oldHarvesterAddress: address, _newHarvesterAddress: address) <<InitializableAbstractStrategy>> +    <<event>> MaxSlippageUpdated(newMaxSlippage: uint256) <<BaseCurveAMOStrategy>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyVault() <<InitializableAbstractStrategy>> +    <<modifier>> onlyHarvester() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernor() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernorOrStrategist() <<InitializableAbstractStrategy>> +    <<modifier>> onlyStrategist() <<BaseCurveAMOStrategy>> +    <<modifier>> improvePoolBalance() <<BaseCurveAMOStrategy>> +    constructor() <<Governable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    constructor(_config: BaseStrategyConfig) <<InitializableAbstractStrategy>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    supportsAsset(_asset: address): bool <<BaseCurveAMOStrategy>> +    constructor(_baseConfig: BaseStrategyConfig, _os: address, _ws: address, _gauge: address, _gaugeFactory: address, _osCoinIndex: uint128, _wsCoinIndex: uint128) <<SonicCurveAMOStrategy>> + + + diff --git a/contracts/docs/SonicCurveAMOStrategyStorage.svg b/contracts/docs/SonicCurveAMOStrategyStorage.svg new file mode 100644 index 0000000000..ec832121c3 --- /dev/null +++ b/contracts/docs/SonicCurveAMOStrategyStorage.svg @@ -0,0 +1,129 @@ + + + + + + +StorageDiagram + + + +3 + +SonicCurveAMOStrategy <<Contract>> + +slot + +0 + +1-50 + +51 + +52 + +53 + +54 + +55 + +56 + +57 + +58 + +59-156 + +157 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.______gap (1600) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_platformAddress (20) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_vaultAddress (20) + +mapping(address=>address): InitializableAbstractStrategy.assetToPToken (32) + +address[]: InitializableAbstractStrategy.assetsMapped (32) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_rewardTokenAddress (20) + +uint256: InitializableAbstractStrategy._deprecated_rewardLiquidationThreshold (32) + +unallocated (12) + +address: InitializableAbstractStrategy.harvesterAddress (20) + +address[]: InitializableAbstractStrategy.rewardTokenAddresses (32) + +int256[98]: InitializableAbstractStrategy._reserved (3136) + +uint256: BaseCurveAMOStrategy.maxSlippage (32) + + + +1 + +address[]: assetsMapped <<Array>> +0x4a11f94e20a93c79f6ec743a1954ec4fc2c08429ae2122118bf234b2185c81b8 + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +3:8->1 + + + + + +2 + +address[]: rewardTokenAddresses <<Array>> +0xa2999d817b6757290b50e8ecf3fa939673403dd35c97de392fdb343b4015ce9e + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +3:13->2 + + + + + diff --git a/contracts/docs/SonicSwapXAMOStrategyHierarchy.svg b/contracts/docs/SonicSwapXAMOStrategyHierarchy.svg new file mode 100644 index 0000000000..c6b6b8b24f --- /dev/null +++ b/contracts/docs/SonicSwapXAMOStrategyHierarchy.svg @@ -0,0 +1,61 @@ + + + + + + +UmlClassDiagram + + + +21 + +Governable +../contracts/governance/Governable.sol + + + +412 + +SonicSwapXAMOStrategy +../contracts/strategies/sonic/SonicSwapXAMOStrategy.sol + + + +290 + +<<Abstract>> +InitializableAbstractStrategy +../contracts/utils/InitializableAbstractStrategy.sol + + + +412->290 + + + + + +289 + +<<Abstract>> +Initializable +../contracts/utils/Initializable.sol + + + +290->21 + + + + + +290->289 + + + + + diff --git a/contracts/docs/SonicSwapXAMOStrategySquashed.svg b/contracts/docs/SonicSwapXAMOStrategySquashed.svg new file mode 100644 index 0000000000..0f57d114b3 --- /dev/null +++ b/contracts/docs/SonicSwapXAMOStrategySquashed.svg @@ -0,0 +1,117 @@ + + + + + + +UmlClassDiagram + + + +412 + +SonicSwapXAMOStrategy +../contracts/strategies/sonic/SonicSwapXAMOStrategy.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _deprecated_platformAddress: address <<InitializableAbstractStrategy>> +   _deprecated_vaultAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardTokenAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardLiquidationThreshold: uint256 <<InitializableAbstractStrategy>> +   _reserved: int256[98] <<InitializableAbstractStrategy>> +Internal: +   assetsMapped: address[] <<InitializableAbstractStrategy>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   platformAddress: address <<InitializableAbstractStrategy>> +   vaultAddress: address <<InitializableAbstractStrategy>> +   assetToPToken: mapping(address=>address) <<InitializableAbstractStrategy>> +   harvesterAddress: address <<InitializableAbstractStrategy>> +   rewardTokenAddresses: address[] <<InitializableAbstractStrategy>> +   SOLVENCY_THRESHOLD: uint256 <<SonicSwapXAMOStrategy>> +   PRECISION: uint256 <<SonicSwapXAMOStrategy>> +   ws: address <<SonicSwapXAMOStrategy>> +   os: address <<SonicSwapXAMOStrategy>> +   pool: address <<SonicSwapXAMOStrategy>> +   gauge: address <<SonicSwapXAMOStrategy>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _initialize(_rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<InitializableAbstractStrategy>> +    _collectRewardTokens() <<InitializableAbstractStrategy>> +    _setPTokenAddress(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    _abstractSetPToken(_asset: address, _pToken: address) <<SonicSwapXAMOStrategy>> +    _deposit(_wS: address, _wsAmount: uint256) <<SonicSwapXAMOStrategy>> +    _calcTokensToMint(_wsAmount: uint256): (osAmount: uint256) <<SonicSwapXAMOStrategy>> +    _calcTokensToBurn(_wsAmount: uint256): (lpTokens: uint256) <<SonicSwapXAMOStrategy>> +    _depositToPoolAndGauge(_wsAmount: uint256, osAmount: uint256): (lpTokens: uint256) <<SonicSwapXAMOStrategy>> +    _withdrawFromGaugeAndPool(lpTokens: uint256) <<SonicSwapXAMOStrategy>> +    _swapExactTokensForTokens(_amountIn: uint256, _tokenIn: address, _tokenOut: address) <<SonicSwapXAMOStrategy>> +    _lpValue(lpTokens: uint256): (value: uint256) <<SonicSwapXAMOStrategy>> +    _invariant(x: uint256, y: uint256): (k: uint256) <<SonicSwapXAMOStrategy>> +    _solvencyAssert() <<SonicSwapXAMOStrategy>> +    _approveBase() <<SonicSwapXAMOStrategy>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    collectRewardTokens() <<onlyHarvester, nonReentrant>> <<SonicSwapXAMOStrategy>> +    setRewardTokenAddresses(_rewardTokenAddresses: address[]) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    getRewardTokenAddresses(): address[] <<InitializableAbstractStrategy>> +    setPTokenAddress(_asset: address, _pToken: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    removePToken(_assetIndex: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    setHarvesterAddress(_harvesterAddress: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    safeApproveAllTokens() <<onlyGovernor, nonReentrant>> <<SonicSwapXAMOStrategy>> +    deposit(_asset: address, _amount: uint256) <<onlyVault, nonReentrant, skimPool>> <<SonicSwapXAMOStrategy>> +    depositAll() <<onlyVault, nonReentrant, skimPool>> <<SonicSwapXAMOStrategy>> +    withdraw(_recipient: address, _asset: address, _wsAmount: uint256) <<onlyVault, nonReentrant, skimPool>> <<SonicSwapXAMOStrategy>> +    withdrawAll() <<onlyVaultOrGovernor, nonReentrant, skimPool>> <<SonicSwapXAMOStrategy>> +    initialize(_rewardTokenAddresses: address[]) <<onlyGovernor, initializer>> <<SonicSwapXAMOStrategy>> +    swapAssetsToPool(_wsAmount: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance, skimPool>> <<SonicSwapXAMOStrategy>> +    swapOTokensToPool(_osAmount: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance, skimPool>> <<SonicSwapXAMOStrategy>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> PTokenAdded(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> PTokenRemoved(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> Deposit(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> Withdrawal(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenCollected(recipient: address, rewardToken: address, amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenAddressesUpdated(_oldAddresses: address[], _newAddresses: address[]) <<InitializableAbstractStrategy>> +    <<event>> HarvesterAddressesUpdated(_oldHarvesterAddress: address, _newHarvesterAddress: address) <<InitializableAbstractStrategy>> +    <<event>> SwapOTokensToPool(osMinted: uint256, wsLiquidity: uint256, osLiquidity: uint256, lpTokens: uint256) <<SonicSwapXAMOStrategy>> +    <<event>> SwapAssetsToPool(wsSwapped: uint256, lpTokens: uint256, osBurnt: uint256) <<SonicSwapXAMOStrategy>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyVault() <<InitializableAbstractStrategy>> +    <<modifier>> onlyHarvester() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernor() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernorOrStrategist() <<InitializableAbstractStrategy>> +    <<modifier>> onlyStrategist() <<SonicSwapXAMOStrategy>> +    <<modifier>> skimPool() <<SonicSwapXAMOStrategy>> +    <<modifier>> improvePoolBalance() <<SonicSwapXAMOStrategy>> +    constructor() <<Governable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    constructor(_config: BaseStrategyConfig) <<InitializableAbstractStrategy>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    checkBalance(_asset: address): (balance: uint256) <<SonicSwapXAMOStrategy>> +    supportsAsset(_asset: address): bool <<SonicSwapXAMOStrategy>> +    constructor(_baseConfig: BaseStrategyConfig, _os: address, _ws: address, _gauge: address) <<SonicSwapXAMOStrategy>> + + + diff --git a/contracts/docs/SonicSwapXAMOStrategyStorage.svg b/contracts/docs/SonicSwapXAMOStrategyStorage.svg new file mode 100644 index 0000000000..a1529c3b6a --- /dev/null +++ b/contracts/docs/SonicSwapXAMOStrategyStorage.svg @@ -0,0 +1,125 @@ + + + + + + +StorageDiagram + + + +3 + +SonicSwapXAMOStrategy <<Contract>> + +slot + +0 + +1-50 + +51 + +52 + +53 + +54 + +55 + +56 + +57 + +58 + +59-156 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.______gap (1600) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_platformAddress (20) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_vaultAddress (20) + +mapping(address=>address): InitializableAbstractStrategy.assetToPToken (32) + +address[]: InitializableAbstractStrategy.assetsMapped (32) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_rewardTokenAddress (20) + +uint256: InitializableAbstractStrategy._deprecated_rewardLiquidationThreshold (32) + +unallocated (12) + +address: InitializableAbstractStrategy.harvesterAddress (20) + +address[]: InitializableAbstractStrategy.rewardTokenAddresses (32) + +int256[98]: InitializableAbstractStrategy._reserved (3136) + + + +1 + +address[]: assetsMapped <<Array>> +0x4a11f94e20a93c79f6ec743a1954ec4fc2c08429ae2122118bf234b2185c81b8 + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +3:8->1 + + + + + +2 + +address[]: rewardTokenAddresses <<Array>> +0xa2999d817b6757290b50e8ecf3fa939673403dd35c97de392fdb343b4015ce9e + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +3:13->2 + + + + + diff --git a/contracts/docs/generate.sh b/contracts/docs/generate.sh index ceb6776774..e2d56117ac 100644 --- a/contracts/docs/generate.sh +++ b/contracts/docs/generate.sh @@ -118,6 +118,14 @@ sol2uml .. -v -hv -hf -he -hs -hl -hi -b SonicStakingStrategy -o SonicStakingStr sol2uml .. -s -d 0 -b SonicStakingStrategy -o SonicStakingStrategySquashed.svg sol2uml storage .. -c SonicStakingStrategy -o SonicStakingStrategyStorage.svg --hideExpand __gap,______gap,_reserved +sol2uml .. -v -hv -hf -he -hs -hl -hi -b SonicCurveAMOStrategy -o SonicCurveAMOStrategyHierarchy.svg +sol2uml .. -s -d 0 -b SonicCurveAMOStrategy -o SonicCurveAMOStrategySquashed.svg +sol2uml storage .. -c SonicCurveAMOStrategy -o SonicCurveAMOStrategyStorage.svg --hideExpand __gap,______gap,_reserved + +sol2uml .. -v -hv -hf -he -hs -hl -hi -b SonicSwapXAMOStrategy -o SonicSwapXAMOStrategyHierarchy.svg +sol2uml .. -s -d 0 -b SonicSwapXAMOStrategy -o SonicSwapXAMOStrategySquashed.svg +sol2uml storage .. -c SonicSwapXAMOStrategy -o SonicSwapXAMOStrategyStorage.svg --hideExpand __gap,______gap,_reserved + # contracts/swapper sol2uml .. -v -hv -hf -he -hs -hl -b Swapper1InchV5 -o Swapper1InchV5Hierarchy.svg sol2uml .. -s -d 0 -b Swapper1InchV5 -o Swapper1InchV5Squashed.svg diff --git a/contracts/docs/plantuml/sonicContracts.png b/contracts/docs/plantuml/sonicContracts.png index 895b1ae3f447bc66b74c92b4b33247755d1a9dd1..747f249de102197a6b6f1cd5bc6b6a68f013683e 100644 GIT binary patch literal 60950 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

fGj;W= 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 HcAWkXCgytB literal 53105 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%0D3?lxgCpKLdrbqo?GnHXsQxd%tc1@o_&c7^ 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 { lp: OSwS-gauge } +object "SwapX AMO Strategy" as swapXAmoStrat <><> #$originColor { + asset: wS + reward: SWPx +} + +object "SwapX OS/wS\nPool" as swapXPool <> { + assets: OS, wS + lp: OSwS +} + +object "SwapX OS/wS\nGauge" as swapXGauge <> { + asset: OSwS + lp: OSwS-gauge +} wos <. zap zap ..> os @@ -96,5 +110,8 @@ harv <..> curveAmoStrat curveAmoStrat ..> curvePool curveAmoStrat ..> curveGauge +harv <..> swapXAmoStrat +swapXAmoStrat ..> swapXPool +swapXAmoStrat ..> swapXGauge @enduml \ No newline at end of file From 9ecd643c846d41743905534a0e772db787b14a37 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 22:06:51 +1100 Subject: [PATCH 72/80] Added Slither ignore --- .../strategies/sonic/SonicSwapXAMOStrategy.sol | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 56a652eeb7..20786d318a 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -628,12 +628,15 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { value = (lpTokens * totalValueOfPool) / totalSupply; } - /// @dev Compute the invariant for a SwapX stable pool. - /// This assumed both x and y tokens are to 18 decimals which is checked in the constructor. - /// invariant: k = x^3 * y + x * y^3 - /// @param x The amount of Wrapped S (wS) tokens in the pool - /// @param y The amount of the OS tokens in the pool - /// @return k The invariant of the SwapX stable pool + /** + * @dev Compute the invariant for a SwapX stable pool. + * This assumed both x and y tokens are to 18 decimals which is checked in the constructor. + * invariant: k = x^3 * y + x * y^3 + * @dev This implementation is copied from SwapX's Pair contract. + * @param x The amount of Wrapped S (wS) tokens in the pool + * @param y The amount of the OS tokens in the pool + * @return k The invariant of the SwapX stable pool + */ function _invariant(uint256 x, uint256 y) internal pure @@ -641,6 +644,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { { uint256 _a = (x * y) / PRECISION; uint256 _b = ((x * x) / PRECISION + (y * y) / PRECISION); + // slither-disable-next-line divide-before-multiply k = (_a * _b) / PRECISION; } From c138b89e8ca2a6b3f90066765b02f854aaafe57b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 7 Mar 2025 22:13:50 +1100 Subject: [PATCH 73/80] Using safeTransfer for SwapX AMO --- .../strategies/sonic/SonicSwapXAMOStrategy.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 20786d318a..f403bbeaf3 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -7,6 +7,7 @@ pragma solidity ^0.8.0; * @author Origin Protocol Inc */ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { StableMath } from "../../utils/StableMath.sol"; @@ -17,6 +18,7 @@ import { IGauge } from "../../interfaces/sonic/ISwapXGauge.sol"; import { IVault } from "../../interfaces/IVault.sol"; contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { + using SafeERC20 for IERC20; using StableMath for uint256; using SafeCast for uint256; @@ -270,7 +272,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IERC20(ws).balanceOf(address(this)) >= _wsAmount, "Not enough wS removed from pool" ); - IERC20(ws).transfer(_recipient, _wsAmount); + IERC20(ws).safeTransfer(_recipient, _wsAmount); // Ensure solvency of the vault _solvencyAssert(); @@ -312,7 +314,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { // This includes all that was removed from the SwapX pool and // any that was sitting in the strategy contract before the removal. uint256 wsBalance = IERC20(ws).balanceOf(address(this)); - IERC20(ws).transfer(vaultAddress, wsBalance); + IERC20(ws).safeTransfer(vaultAddress, wsBalance); // Emit event for the withdrawn wS tokens emit Withdrawal(ws, pool, wsBalance); @@ -530,9 +532,9 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { returns (uint256 lpTokens) { // Transfer wS to the pool - IERC20(ws).transfer(pool, _wsAmount); + IERC20(ws).safeTransfer(pool, _wsAmount); // Transfer OS to the pool - IERC20(os).transfer(pool, osAmount); + IERC20(os).safeTransfer(pool, osAmount); // Mint LP tokens from the pool lpTokens = IPair(pool).mint(address(this)); @@ -555,7 +557,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { IGauge(gauge).withdraw(lpTokens); // Transfer the pool LP tokens to the pool - IERC20(pool).transfer(pool, lpTokens); + IERC20(pool).safeTransfer(pool, lpTokens); // Burn the LP tokens and transfer the wS and OS back to the strategy IPair(pool).burn(address(this)); @@ -573,7 +575,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { address _tokenOut ) internal { // Transfer in tokens to the pool - IERC20(_tokenIn).transfer(pool, _amountIn); + IERC20(_tokenIn).safeTransfer(pool, _amountIn); // Calculate how much out tokens we get from the swap uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); From 280a70b3b4a1a62336be1421c402ff04ae9f6061 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 11 Mar 2025 14:01:53 +1100 Subject: [PATCH 74/80] Swapped the deployment order of Curve AMO and SwapX AMO --- .../{009_curve_amo.js => 009_swapx_amo.js} | 118 ++++++++---------- .../{014_swapx_amo.js => 014_curve_amo.js} | 64 ++++++---- 2 files changed, 92 insertions(+), 90 deletions(-) rename contracts/deploy/sonic/{009_curve_amo.js => 009_swapx_amo.js} (65%) rename contracts/deploy/sonic/{014_swapx_amo.js => 014_curve_amo.js} (52%) diff --git a/contracts/deploy/sonic/009_curve_amo.js b/contracts/deploy/sonic/009_swapx_amo.js similarity index 65% rename from contracts/deploy/sonic/009_curve_amo.js rename to contracts/deploy/sonic/009_swapx_amo.js index 4b2b777ed9..25d648b087 100644 --- a/contracts/deploy/sonic/009_curve_amo.js +++ b/contracts/deploy/sonic/009_swapx_amo.js @@ -4,66 +4,15 @@ const { withConfirmation, } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); -const { oethUnits } = require("../../test/helpers"); module.exports = deployOnSonic( { - deployName: "009_curve_amo", + deployName: "009_swapx_amo", }, async ({ ethers }) => { const { deployerAddr, strategistAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - // Deploy Sonic Curve AMO Strategy proxy - const cOSonicProxy = await ethers.getContract("OSonicProxy"); - const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); - const cOSonicVaultAdmin = await ethers.getContractAt( - "OSonicVaultAdmin", - cOSonicVaultProxy.address - ); - - const dSonicCurveAMOStrategyProxy = await deployWithConfirmation( - "SonicCurveAMOStrategyProxy", - [] - ); - - const cSonicCurveAMOStrategyProxy = await ethers.getContract( - "SonicCurveAMOStrategyProxy" - ); - - // Deploy Sonic Curve AMO Strategy implementation - const dSonicCurveAMOStrategy = await deployWithConfirmation( - "SonicCurveAMOStrategy", - [ - [addresses.sonic.WS_OS.pool, cOSonicVaultProxy.address], - cOSonicProxy.address, - addresses.sonic.wS, - addresses.sonic.WS_OS.gauge, - addresses.sonic.childLiquidityGaugeFactory, - 0, // The OToken (OS for Sonic) is coin 0 of the Curve OS/wS pool - 1, // The WETH token (wS for Sonic) is coin 1 of the Curve OS/wS pool - ] - ); - const cSonicCurveAMOStrategy = await ethers.getContractAt( - "SonicCurveAMOStrategy", - dSonicCurveAMOStrategyProxy.address - ); - - // Initialize Sonic Curve AMO Strategy implementation - const initData = cSonicCurveAMOStrategy.interface.encodeFunctionData( - "initialize(address[],uint256)", - [[addresses.sonic.CRV], oethUnits("0.002")] - ); - await withConfirmation( - // prettier-ignore - cSonicCurveAMOStrategyProxy - .connect(sDeployer)["initialize(address,address,bytes)"]( - dSonicCurveAMOStrategy.address, - addresses.sonic.timelock, - initData - ) - ); - // Deploy a new Vault Core implementation const dOSonicVaultCore = await deployWithConfirmation("OSonicVaultCore", [ addresses.sonic.wS, @@ -101,14 +50,57 @@ module.exports = deployOnSonic( // Initialize the Harvester // prettier-ignore await withConfirmation( - cHarvesterProxy + cHarvesterProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dHarvester.address, + addresses.sonic.timelock, + initSonicStakingStrategy + ) + ); + + // Deploy Sonic SwapX AMO Strategy proxy + const cOSonicProxy = await ethers.getContract("OSonicProxy"); + const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); + const cOSonicVaultAdmin = await ethers.getContractAt( + "OSonicVaultAdmin", + cOSonicVaultProxy.address + ); + const dSonicSwapXAMOStrategyProxy = await deployWithConfirmation( + "SonicSwapXAMOStrategyProxy", + [] + ); + const cSonicSwapXAMOStrategyProxy = await ethers.getContract( + "SonicSwapXAMOStrategyProxy" + ); + + // Deploy Sonic SwapX AMO Strategy implementation + const dSonicSwapXAMOStrategy = await deployWithConfirmation( + "SonicSwapXAMOStrategy", + [ + [addresses.sonic.SwapXWSOS.pool, cOSonicVaultProxy.address], + cOSonicProxy.address, + addresses.sonic.wS, + addresses.sonic.SwapXWSOS.gauge, + ] + ); + const cSonicSwapXAMOStrategy = await ethers.getContractAt( + "SonicSwapXAMOStrategy", + dSonicSwapXAMOStrategyProxy.address + ); + // Initialize Sonic Curve AMO Strategy implementation + const initData = cSonicSwapXAMOStrategy.interface.encodeFunctionData( + "initialize(address[])", + [[addresses.sonic.SWPx]] + ); + await withConfirmation( + // prettier-ignore + cSonicSwapXAMOStrategyProxy .connect(sDeployer)["initialize(address,address,bytes)"]( - dHarvester.address, + dSonicSwapXAMOStrategy.address, addresses.sonic.timelock, - initSonicStakingStrategy + initData ) ); - return { actions: [ // 1. Upgrade Vault proxy to new VaultCore @@ -123,27 +115,27 @@ module.exports = deployOnSonic( signature: "setAdminImpl(address)", args: [dOSonicVaultAdmin.address], }, - // 3. Approve strategy on vault + // 3. Approve new strategy on the Vault { contract: cOSonicVaultAdmin, signature: "approveStrategy(address)", - args: [cSonicCurveAMOStrategyProxy.address], + args: [cSonicSwapXAMOStrategyProxy.address], }, // 4. Add strategy to mint whitelist { contract: cOSonicVaultAdmin, signature: "addStrategyToMintWhitelist(address)", - args: [cSonicCurveAMOStrategyProxy.address], + args: [cSonicSwapXAMOStrategyProxy.address], }, - // 5. Enable for Curve AMO after it has been deployed + // 5. Enable for SwapX AMO after it has been deployed { contract: cHarvester, signature: "setSupportedStrategy(address,bool)", - args: [cSonicCurveAMOStrategyProxy.address, true], + args: [cSonicSwapXAMOStrategyProxy.address, true], }, - // 6. Set the Harvester on the Curve AMO strategy + // 6. Set the Harvester on the SwapX AMO strategy { - contract: cSonicCurveAMOStrategy, + contract: cSonicSwapXAMOStrategy, signature: "setHarvesterAddress(address)", args: [cHarvesterProxy.address], }, diff --git a/contracts/deploy/sonic/014_swapx_amo.js b/contracts/deploy/sonic/014_curve_amo.js similarity index 52% rename from contracts/deploy/sonic/014_swapx_amo.js rename to contracts/deploy/sonic/014_curve_amo.js index c0bc556eec..908665cca2 100644 --- a/contracts/deploy/sonic/014_swapx_amo.js +++ b/contracts/deploy/sonic/014_curve_amo.js @@ -4,15 +4,17 @@ const { withConfirmation, } = require("../../utils/deploy"); const addresses = require("../../utils/addresses"); +const { oethUnits } = require("../../test/helpers"); module.exports = deployOnSonic( { - deployName: "014_swapx_amo", + deployName: "014_curve_amo", }, async ({ ethers }) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); - // Deploy Sonic SwapX AMO Strategy proxy + + // Get exiting contracts const cOSonicProxy = await ethers.getContract("OSonicProxy"); const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); const cOSonicVaultAdmin = await ethers.getContractAt( @@ -24,65 +26,73 @@ module.exports = deployOnSonic( "OETHHarvesterSimple", cHarvesterProxy.address ); - const dSonicSwapXAMOStrategyProxy = await deployWithConfirmation( - "SonicSwapXAMOStrategyProxy", + + // Deploy Sonic Curve AMO Strategy proxy + const dSonicCurveAMOStrategyProxy = await deployWithConfirmation( + "SonicCurveAMOStrategyProxy", [] ); - const cSonicSwapXAMOStrategyProxy = await ethers.getContract( - "SonicSwapXAMOStrategyProxy" + + const cSonicCurveAMOStrategyProxy = await ethers.getContract( + "SonicCurveAMOStrategyProxy" ); - // Deploy Sonic SwapX AMO Strategy implementation - const dSonicSwapXAMOStrategy = await deployWithConfirmation( - "SonicSwapXAMOStrategy", + // Deploy Sonic Curve AMO Strategy implementation + const dSonicCurveAMOStrategy = await deployWithConfirmation( + "SonicCurveAMOStrategy", [ - [addresses.sonic.SwapXWSOS.pool, cOSonicVaultProxy.address], + [addresses.sonic.WS_OS.pool, cOSonicVaultProxy.address], cOSonicProxy.address, addresses.sonic.wS, - addresses.sonic.SwapXWSOS.gauge, + addresses.sonic.WS_OS.gauge, + addresses.sonic.childLiquidityGaugeFactory, + 0, // The OToken (OS for Sonic) is coin 0 of the Curve OS/wS pool + 1, // The WETH token (wS for Sonic) is coin 1 of the Curve OS/wS pool ] ); - const cSonicSwapXAMOStrategy = await ethers.getContractAt( - "SonicSwapXAMOStrategy", - dSonicSwapXAMOStrategyProxy.address + const cSonicCurveAMOStrategy = await ethers.getContractAt( + "SonicCurveAMOStrategy", + dSonicCurveAMOStrategyProxy.address ); + // Initialize Sonic Curve AMO Strategy implementation - const initData = cSonicSwapXAMOStrategy.interface.encodeFunctionData( - "initialize(address[])", - [[addresses.sonic.SWPx]] + const initData = cSonicCurveAMOStrategy.interface.encodeFunctionData( + "initialize(address[],uint256)", + [[addresses.sonic.CRV], oethUnits("0.002")] ); await withConfirmation( // prettier-ignore - cSonicSwapXAMOStrategyProxy + cSonicCurveAMOStrategyProxy .connect(sDeployer)["initialize(address,address,bytes)"]( - dSonicSwapXAMOStrategy.address, + dSonicCurveAMOStrategy.address, addresses.sonic.timelock, initData ) ); + return { actions: [ - // 1. Approve new strategy on the Vault + // 3. Approve strategy on vault { contract: cOSonicVaultAdmin, signature: "approveStrategy(address)", - args: [cSonicSwapXAMOStrategyProxy.address], + args: [cSonicCurveAMOStrategyProxy.address], }, - // 2. Add strategy to mint whitelist + // 4. Add strategy to mint whitelist { contract: cOSonicVaultAdmin, signature: "addStrategyToMintWhitelist(address)", - args: [cSonicSwapXAMOStrategyProxy.address], + args: [cSonicCurveAMOStrategyProxy.address], }, - // 3. Enable for SwapX AMO after it has been deployed + // 5. Enable for Curve AMO after it has been deployed { contract: cHarvester, signature: "setSupportedStrategy(address,bool)", - args: [cSonicSwapXAMOStrategyProxy.address, true], + args: [cSonicCurveAMOStrategyProxy.address, true], }, - // 4. Set the Harvester on the SwapX AMO strategy + // 6. Set the Harvester on the Curve AMO strategy { - contract: cSonicSwapXAMOStrategy, + contract: cSonicCurveAMOStrategy, signature: "setHarvesterAddress(address)", args: [cHarvesterProxy.address], }, From f901dcf2ab543a5cd91b209791dac0ebbb221a81 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 11 Mar 2025 14:03:12 +1100 Subject: [PATCH 75/80] Added SWPx the Harvester in Sonic contract diagram --- contracts/docs/plantuml/sonicContracts.png | Bin 60950 -> 61878 bytes contracts/docs/plantuml/sonicContracts.puml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/docs/plantuml/sonicContracts.png b/contracts/docs/plantuml/sonicContracts.png index 747f249de102197a6b6f1cd5bc6b6a68f013683e..03924d20e30e919de3d10b6ce84b4c323f873cd4 100644 GIT binary patch literal 61878 zcmcG#WmuGL+ct_~3nCz05`u_;bc1v#As`)6(j7w#p`;)s-6Gx6F)$$A-8FQ>&>i17 z-uLso>-~PMA8XB~4i49xam0S~Hw8IK%tyqJ(9qB@rKQ9a(a_K{z+a&Ux4|cw@I`j; z_R2wA&B4Ih#>K+uvjdu>k(H61o`aF$bA6ZRCJqiZ_Por@HWqqT4vv-E#&LJ>=o`^kFF_WJ5AA%=vR$` z^V15XJ=mV|ZYa`6ys&xwhtR0`T=~(~fM|4d3;vJyzkZoTic*#8KObxUC{tMCq@pIq zidcU@ibt)G{W}huR(SkTx$#-JkOUm_AwGc%e?k0nR@9m_hgZa^x*_d5IA<8?pgP)0smj$||Hs$*%R5 zz(ZVUvl6ehvW}8z<>9krSTnrg3FRB;OGDCrbuL1A(gz$ql)QaiWe*fF?#j>M*t#ak zeDx0tBfH`MeSev<+-?xJcam)P5oW=KMS8O4=6h?p6Dk9`)9MG;KeRJ`5T%v}bQ_(1 zG(Mi*9&ML4>(TF!cs0(D+{WGHxMDZ=$b{UHxneze`mVgE?lbz*og1n2bMEuV(2sWC zqhy#2l%n{@<7A#xs+d09)riv4v>a#ZvBEqATuGkJ|KG)>;4sh?DlCSL7DC)NDe`uy0f zu>I|6GwxjJo^#hRK(JQe>s8vyyodb_0XK}h{w@hZZ%TD$bMKbnt1(Iu8rC#%A+9^c zO@jL@{s~&ddVH|*3hODpr&_~uRbd9HQKYHtTLwkiw14g!eN`rkfjU&3=ny^#Gwkm- zFSUo-H0ri1hz}Db*g)K>PZ(gCLKi%t-yXa$r>E+VpiP`9_C$;RVPd+-uAPU5=7lCL zCaml{xs~)-3(x4hO?Q}GR%uANHkB$n`{k?IM$94;;#V)sX0uyrgM5GJXJ^y+`lg!x zihN0IeW6Q8hI+D=RBUuFHiW<`4=Yatexi zqps-T;bATW^xy(nKi1C!z1Xhoy9#|wzyv*=_vf;n%|}p0MMh3LlAL>NzL_HSYyqc1 z+(jG43c_bJ>;L)XltNHYu)L#ZY%GSAQm@=2BE|FCWwA9F+&n8AgWfqf$aZtIfVuv1 zf6BVPAg@%}$Zx;l$aK1DFP_8P1MJ}n{T}{;o{6cj$-;aW5B=|56|8HIMm3k1f{8iK zuaKTM;7`IeIluF7P9l%}4m#B$os9f^gyB%8tn$7686O`XX87r^va&-g&r?xF#X%Sn zuDPiHK9M_o|9fa?Q6BC`Bfr7hs)br-y?jVgJ|~Nj+>d||?5p$s6i%#&fT(0J1C2x%6se;Yiq0n}j5rRW3fkTCQk1JMIR(xii zX}J5vhct=smz&JI?&oEfr$h2WmgB!u0RtkXW0lj#0CM#xy7>h=j{ZYpTLBfy5@+4JQgAQ>FyqiYvdn09PuM?}z;m6EHi(ms`ZHDO)# z($76>oA2c1+v-WsP=cC$U&=(I%tTm5Y*6~Ms9g?|>)e<7CW0=r`Rb+d?4}|^q5S&+ z@TqX+0TsnNS65d@1UbyGtoLzLQRbJYd&0uP2Cd&&UN*D@5iCDSm!y5C=B*z#Tmyp* zNeT(opB=2;oGylLbf#C7FvV>}ciWxr&VTsup^UgEk=HS|nl+*Ow+zDl=IX>q(Cv6D zH8c`ScF2tNW@iR=v){$M;djWWFtK#2rnf&S(f%&f_Ti0JX`9C2`A3fZfP#W!9URmknv93G9 zcjy1h&p-DmvRhFk^Ss*BEfK%7Hv>TswTH!j)6g^Re#cx#!w_pdQ)5(H*4py?537mC zbJd-Jv^Gf!cN*XKBcrvo{2Ie0M(-XQarT0nTpf++N>iDxOqQG5+u51?(rfYqx`)ZQ zFj6&H)(?&T$vCn+vYI&VN-(Y^9sbsQhw zxo)wq#Z;Lob>8+m|6DzgVU%LB>3Wgl}%m)+J;naF`z-^a%|;%E-tl zk)H~umQ-GpmX=OkFM^-TSiQ`nIWjU??M*6S9WnIHUMe5ciAd0`_n2`u4Gz-lh3L90 z<2lOd>CJaWF`%QP*VWb<@-RkL--Ewv-xy3!r-`#nlL(eTNQ~xml^QB&^XfI;Ip>D# zO#eN1lRwtGi`NVc7uL(iFpl<~>6|y$fwG@r&@;QZsH`kHfsuA&aB>uIZQu?*qkqXB z9vk~|MpdPErq-ntP=bB6;S2h`)-;)@Vr1w0crI%zO4czqWE~xskjsAh-XJkSQz9}~ zLt39s_ap-8=j)pTH!tkw*wrqtIGV1uM&T z=U9QB#2F`t7d$XIr?@%2FUw^**entk%vQDqLwH2AUl$=#u@M@Gp{C(1E`ImA>o1gf z)wvvvr~<%>X40-P?n};5%93F|yFBOvqRL@2_cq78Pw*mNSrLa@z0WKInH#5&+!f6@ zRbtcyHnGyWXGc3Blj?qPEH8Lw2!&oAt`A9Hm|PJPP*YNV$()&)`HS*jvD-q((n5i1 z(E4ro}$z!>k*94$P5F{7{9ZW~U9n z(?5UyIP8yLnqJ%Y3DDtH0~Aex?n_4jq_UBhm#;>!7_=$~=Ax9?K1;;Y-F{-Q0{tF_ zTCu*khQ?%f9Gj6p=OyeG8Zee#u3ipee&3>)58Fa1lJ4~zQHy%qw8*Q`xI za~Ho}0g{z`id@b8m?sfJniEp4SJK@|MEv|9Cd}k`eaN^sktfCDJg25nRo5Mlm^hc$ z1dwEXDes)y_0WXZY{GJ9Z6Gb0tJr2POD?Gfz)yx(=L+VXWu?;T_sNgE*-TkA_doH* zmFztv8v}BJ@Vs&V&y~*SS9%gCJTF;z5DkJ(4`F9nM|Yh`~ACLq==&fOXzZU z)(x4nhij($9oX2xeTAAAFJ6?G69`-7$|W$^M-iz!bm2_e2KFS|et1KXmsh<&bD1`} z+Hq6m!-w$d><1$~J)K2;h7~0=Wjz1N4w{Yj8-8ZsuK?$xAx^#8ZaI=($?!ce%_t-J z_w^tDxaLFJ)i5&sqpZ$29)qo6W(F2E67~2c~o#4xhzX zuYY#hX58_NrArp1sz`oTOho0(!sQr7v8*!SUnS$*;Z%GU>wNy(%E(c_Eny+`L9uH} z)hQd4;GZjaAtzZ1T%}*T%57cbRaxgsxdxfrQ=h14=q-8ckD~{kR#ZtI!sK6%XG1YU z7r#k|21Z-9E-ZA6(^Rf(Z9vz8k2;={ zz-@M!2Y03TdZXw-g!7%*A?2R_6ZynTiMS#Uj6}+Np-P`K@prpbi#DP+tfMZ; z<@>amYYz3BRJ4iy6)!}=V%f4!6)c#0H`QcB&O)%)zO@f8$yG(excyGyo`9R^Yxggk zUL$84ZP-f9K4+cXc+v~KuqxA{8fDMCkqc)C`(7tP9L6-=4e3|QpnvnP+YL95U}yP$ z5oamTq2hW?w-Y3hFVGOgH2W;n5*w2^aGT(4v5H4uIlxNvXfe`vA^C~^*^^#>6>>r~ z)0}ISvc|Bz{IT@mhpW{p&K?-&d4FB?28d-`Z*<^N{jS&oGD_-kX#VnV7^6mzL~!-Z zM4^_EY^b2j4V!o9eW3*_W5K4DM$2j5g-T5Z#t}sBi2{o;+5Hn)mlR0ZqHMD;dk$71!+iaPnoVItK>2%e2X*#* z;=j=c!ZG<7c^kEYMg2>By4`+rc>N!qFaOy>%v3pm@YsuW#ynt`qzPA^VqTA4pUr{# zmn9FSh)RIJDm{xUTRp*J&cK-251i2J9|7YpILYxgpKRFaR4Wnm6a0tAQk&Hic;_=RwPU4Cdg$y$h>T<+I)$7(zPJ)+}} zziIh9JpHjL(kzQr3YWVlJaKN^0*RjAviO9&jeCAbAaEOIGIO*`?&O>b&T5#w{~hKj6RWM7Xqn^H=?* za$+a@X6z9KzLnKNFoiS5V;n>j--H9wtfRcv!@&79*MAkk&s2^b>vrunM&;1V0aKsL z4(SCG&*o^vR*no-1!=PMtJgyJ{9Xl=jLAHZy0yWenRqdE82}Y1-l757{i_q0V`CL&ZRoeVdd&)^ND3$uQn`O^~@bsmz7;y*@Yf0hCL+B-8vo9;@ezqi)v;+hT(4Eet5k>8>C4;K*y zH_sp%q-S?l|13kfuGn^txGiuhR|Q>zr_68<4c_RmEnCaH;fv_lS2RAHp)|+2Osk+a zrAkH80}ciGEak!zuJYElnR#7Ra>tO9nx=!6Zxm84UseVRHh*kvBTfonVaWlKF2a{P0A||4{pi^8Cl_!sK*&U6cyp*-xC4rcXZw&tJks z9mji#K*$gDue~m;aMiP(Zv{18R z*-XP5wKH|5&2wWOdvTnb?zJy6+PU>38}6NbBO&NVe<0eaQJg3xLX0@?lQ8l_9d6l@ zup8n8|c!{PFLV!2ghjCKY@-7K*v4>2%h6msCQxVuIuG|FyKtkuFkx_p(I3p8Q|M$ zT%}eOtSNcvWS)_6t~r1^mgwk2{=X>SI&=_ox>CG+M?Qk9%&PAsDh#i)y`lLFR?GS( zztU*W8r_f3&ydYY*?Y{K;WsS_`lG}i)$i>VIizT``WfR%dryS+KTiaFRbf+0UwH$m zvqAoGbhG69)@}&63LP^9W-Flwm@Ri6CY0GiG~}sjJ3%?_*Wp*cWw1nP7(8z;@?&e1 zWl4=u^Rq(X_1VzPj|!qh)?(e=2hEeM*Ko)u~~KZ4-VV!yU^%eIcC0I zy7a~m;JBDDpP8=e@z!ED$u}AOnioc&azU}uV=B2pb>tzv^RAzaeTQ9@lP(d6$}FTn z9$4M@xr@xeTPFw9Lpc5noX{~m4bvvz$?a-yX9+e{**}ymtBFCrFwBOq&uffOE}p@~ z54h4diFwwtcDXYslfF;AvB2@PnAEax)mEPeh=|-z@Q)1DlHa10kR6j|%-Jnf=a-Fs zpUs^45g~Alx7t5JvJKH5hV)&v_uwRtH(6I=T@BrO_uzjNQ}x1h#q7A(-SeXcvslKi z*r4XL@OF->%iWp}*w`=(u0*_#Irf^zMYSHK_^->;+u8l!`~Gx`t&y&T^SX=_HTPWm zj$uq0R!PC{CGFjTj)}XE&Ex*Bi&YI(l3;YNR{A*HJYRlci0+*>7O4>L<1x+}=O@1s zPw3t7Hs>zXpIYzZMRCDwmgb~+z*>I+F`OZRpwEpM{utqlG~3cx68n$J9!FSwey)k= zB#}Q*xhyVK&EUl9(b$dONGub9Gp^UiD=Wq5kn#l3>Rdz~#1X3G6zqoWjqJ{RXRfwL zMxTT2YM&Mk=>(C&|5ZfCz~MBZO_br2ym{W_{8-w$IQItxe)vRnoF;MA&s8@5mF%lU zxjmBBtF!4Zt|L~nPV<~bO?p@_g?o;5u+xk;^o7;dVI}M2!c*|rCdxj;U6<2BGAMz; z`X9)E3<o5}WG}OekU#cGiss(Dp?70WD5AFI@bdK?ma} zrn!1@y{k=w66yxXCb!Wu`?tm2NLluJKE-B74_ZlIZZ~g|TVt6_Z}Rdhc;?7WGmyN) zYOVyGJadupOWtuv>p7UzsqQ_1h)g&;;T%WW{$%fv3$o-FH3MX%&L2Hx9}}rSJ{;M5 zqr5kiywcFfo)uDuW)M{5DEha;Njpv$0yyW>Np#4g@iSSo>?MH5du(2HMG`a;L%b;<=cbYV7MQ(KE|@ zRql6*ZgAZWIZZo7DZ{qnA027jz~AZKA-c&3lwnidUio0~eTqyy;;=q^64|b0HJWvG zb{P@jj`rX@IPUwrc7n3%8OmaVTc%G=Q-h2sE-volM~^HvVIe%xI>$x86HH8<%ouzg zkB5B}$kfESBeP~AqvP%T481F8sc<=aQY&t|>r3$=o(n>ktnzB7#v6LNKb`xn6G@?b zf4}nBD;8Hfk8DKoMFe7TmDE+ub>pK)I^~IW;!h0cCjgkeYK~?y69(6raejaT!FU=i zGW_i94CD=w{i&iVD$zZg5HkDM0z^x_Ke1cRQpUTon3hKgnj@GKoCqd!g+-kl9)oj8 z`*t|chqu8Xm4iMT2ngczi}_Ew?!4ig$U|*+wCob?lDlVVbol4w=A~(7Iq7>VC$h^$ zt<%rVqC8YbEW!$e5O$R2!xE%Gg5O>-;r#gMC_=!b70YTQ<39y5d$1>-o1fjsZnX`% zT|Ktq1x89TQ^{B-cP_pq+Ok^l_?tj9G$zD1jCs0;@du2YFF_Ndt4vUq4gSvb@V&>e zf;U>B)B;kRyrf#CH%&yW{gKi6aOE_%!nSud`Tiwl{&gfCBbiX8A78Fv-wMR6PoSfHkXX8R|A+J|7cusclsJ$I6KLmVaCL-T2)S6#z!!A$L!QL}AucE%fHPOqW zos#bwU7GJx+t;?=ZtUS`=bVtxVqB_G*2#5>%s0)m`fGH8d<;1)C(BSp2gy)!9UYw{ zUPr2-qut$X`-y@^2$|Vyfx*{}b@%zA<3St&LfA7brUkqu8!sA7Gr!=SPtv?VryU($ z+mET%y)?PSC$Qed;JNy_b4h@BeLK;QN!^cYe_@%?g%C?aHKp|sBN^&bhSN@ z=3lo7;b4!R?XVtKG75K&(8VN?YPT|Lbv3M&DyZkbWVsocJ~b<;|D^ngV$NMD&uUrA z08y|LlD`L-l@$uLgKhu0G`lFQ#^@=>&VsH{1|}nR)_;=l!te`f{`!hm+H^IR5iVg= zv4{6G91iWXDE!9GCXuvsbSK+WG0-uPn4H~)lqbKg3czfX;|WCm*hEalB`CqFzh#9g z&vZ`Md+luVOJ}JgFb6)Gjao>aSI14|fJ-~fZ_I`=%qTt1U4^Z%vX4n@inNsx67FH&p7YDf9lC*<>!t@Y*mTI0*b} zHP|p$NVsCz;%*!7ipOcGB$Xb~=thcCJN{7^8IP7C)a_}9ums8^VPG%3g=-e5^s-*< z=&CHg%}cr{R^el9@6mdKKPjJb9-T6K)U>1?(QRbnPO?W_zJZEL5?dfWcU9%jNK`Rw zK5h!g^n9s~qos}`x$mr7k|Zg>G0ulII_Y&GOwx@UNTELH66OwWOA{F^L5?zJBODvg zwP|85QI=Qpys&z1!(H-sZfnxQZiHzgln?pM zgLD#t?b_@%7k>nBfQKah6&&O(<`k0nj%t0D_wH5jJWOaY4xcJcb0}HZPcKx^kXwj9 zWEJVE`3<;`KX4v84sBLl&5=I@&edOw{*xDO&Qx`7pm}OAB}YoFZAto>e@KMJ-W&?K z5TE7OkD0$z|GXGlH3nVb zh&21oD=w~o)&RllEOY~>>8y#){xC5}NHreBWtd-&V5-!pWKcFjT;vMnkejcQ8&Fo& z5EB_sld8yH%{HI2F^aKrJ=wKlJtAtwt2{r;!n0dRdmoHDl|kSi&?fufL~8roDhVrD zQIe<;5{^nS(H&{q{~0OuT*iOWXSn|+mc9NXA3Q5Q(UkH>wXhdt(znB=R@&NDS*q{W zLI$LH9m$QR4rHy}Z7n4*yidIiT`}>}k6H`oapn9IAGdTB_sZV;@;NY!c^E3I>lN{r zP3aZn?vYX}0??>%xA(nePdlccH(ca%SQOJT=fLd5CFN5G%>%f1zZ9PNxbj&Kg`-C# zU106Jt&$wgLTd1Y1={8w{88Fq7X|JXFj{l9aC6G4^J)rNA2c=|6v-ISYj3WPpCcWn zdM{_2rSK&KPlF4pvyoD}Gp-a;E%6k_s5@u<=gwKjz#&^b(1u~A>)UBYq~$3?b|bHE zQCUQ=UL9i3E*=MUw)sc1S-T52)g@>^(!<z^B2_7uuRw>H4gq_Bk5fSzR@pnvqH~_?e4dW z&}X#GP$JO!z z_CVwL=+^3Sh?UiJS;P2$&a@-6 z*Dc;hY{hLkvAt2eP=h(^lm6ut10{*zR0xY$Qhr}2VGn4|ugR>M9SMUk|FEui6V^Hq z`2?CBF+7B@tuJg_#C!3gD5~HtNKQEdp9iw`VF(lzlBz3g2_%c?DeCS$Oqu7my)-Yp zchMu-2`vn9T*NG}=uL1SiE_v?Rm zq)nqgB7ZgQW)nORNK+Udbyas5#lCD^2@Aa0@hrev-Q?{gj5XiYx4I+!t9ns(*6{4f z%r7f^S2sVEBegC8SzYiGU<;> z6J4o|?q#)=Hy7m}NYd+xAqa;U=w8s*r|JdiJAlN&Y4l0gA#NS*ay(PL!<~@2`}Ty& z@y^>yvxfSyR@44|!4B?T`Fk_34A}mG*ITJ3*kr@mVZ2zgK6ij=sJRXNzwq#IZY(sk zpGLrz%6>C&^DUA2fvp4oXo;|NxZh^Mt4Kp0Y?PA(yY@`#r+5ky-%)%mvkdx&?|b#S z9^%F&d|y^JvY?&Fjdfb(d0Ttg9dJgwt-+wmZ74Y@-1-(3DMY?S{AekC` zQP^Q3W44s$BEPwUK~8rbG90}1Hd`D172hV3Wkjp)o~`Cx^7-!I#qb=KA0eKMesW83 zC2iw_1n-TlSfiDqisK9?qZoKyAK|@f{eoqIT?_J$j}eZpBBG+WI5;-sXlQR20qDjn z8nOoETz{%n$t=x~dHS;Rpu1kSz-H0OCR^9S`JngZij;92Vu}d}i1+DErg3GFM<)5; zuk0h-EFrDc3B}|{`#MtWBxx<)qmG`QqmvV628Jfo1+sRvBZLJ6&ew>?s3DwY|7 zhm21ExStlfSZF*ZAtNKh!}HA}1Rn!1{)!yM0v>RwDV~M2^taGZd0Sf+Ce&8|VvpxZsX3_AF>4i$P^y-q$!)>*_Vz%}2j`5qv~&k3?%PnJsi9mnIj!#yI@#ed2oIEbR6KWhZ2VwaXUM^SFc{7M5kdL3bCsqXD#5@y_Gz+?Aa z{u=C;U?H_iJb_kHqMDAGsz_AKKXN@%r|iaWTy(1=5`1t?PhB8Jf@`$_g|$1swIkhy|dY_;nl3V zFINlJKIMEWi;;lD?#>L2Rd+X$zez--)tfj$GPE~ibbCMCyFLvqiBUP(*I6S`FR2^! z`{-e0_RIRqPXiVmhdo+rk-H|tvGoByk5#S+4DFcjK*s@{lH+H82qiE{$x|&l zKRq3iu#bO z(9pIZ&Yc=#>kTDUi#@L%O{5b|jVC%bT4OLPT8=kT*&8L7gpoluaEkY`cZqrv z2rrnDxbz1~{SMZfafn(nyX5W9Llj06--KfR=zN+$dGDUeWYD!4KAiPxNg$RW7}{^M z%g>9?J0kqJIsR&^9N49m)Xv1C$8UGuDVQ$GmBLWD+S_!`X=rFTIPg|MZ8C8f(&3TZ zqbPw!+qj4^#&e3@7wRhB7r3J`J4sj95_CKB+dI5V+hK=+S#P=!_ZIletEf$G_ZF{< zJZGL+$Ik}i<)b|I>J0**Rw{oHh;usEc ztks>`dR112!a2<>8X+L{JNapijyI*buu?Wg_ecsyfAYebe2}AI1QGNFsd5(8L5ST% zyQk>NJ#kpWdT=@xb=>GUXmwfeF|Jet%_W-GE$xcQh89S!#CO6kt)SHjxSDx^%E!$4 z+Io`>$O3|2iq2o5HPT8}WuI!zHy=yWJF}=J{kW~6W?0w_vvavFX?}H{XIbB#C%(m+ zD8wK$cG6pQxj{N%<|6)My^atB`bFJU?ud8mL;p8 zXo{XzoXq`Sz0wSZ6J&V$6&@N%qIC5mb<>a3>m9|CPMI>8>LQWL+84gDVJhwkByC0=I3LKaZF*8k|w64gdTu%7OuiTifR2m z!MD@WMxlVLEaoCVq~aruhTM~O4bZnxeUz&qBtz8ujdNRC;N0FSw2gsc+2hC?gYfj% zvIu95sE-hJ$g^*f1```Ag+(Z6x(NP!rgKPIS+dsOT zv(574s?-dTAx127NZfc^h6IPzPZSdxlYD0-fAJ5qoCmea_KG9El@dOt+uLu*ij%;> z4tc+p--8hP;Dn|ilPLmw?xl&*h~8}&Jv}|}Pma{Q+S}VtPo1^KEbhl|-$wH)z1W$v z{e^6o2^4+wQYa*)>sZ2cLy?6wmY~43!zLo~#05S{76>iYCeSWlOK)?w|9YudAeLC} zhu5Eu91Jl`&_1*M(<&23vNdT(+0iHUzJtsqk&WvKuhWhYuOnL}U=-v!oH`u4XJ_U2 zy`CK4Nb) z3{$(8Jd50OeNwYoL0P~}YeK8)^ZI&IB|~rMsM*UvYH_Vw*87@#Zi~m&=NT(!5lIPs ze@vlVl$5C+43HPa#l=S>UMP11#5*HatX4dqdVZbD9vD{HY)8@6}7aswzq>*pMo!M|Y@=7?}!Z53kMSAGBVlm8G~1CE$J?GK7f@oV}R~rv67nL_`K{ zAuVAVXyaeOzH4hHiT6=GE?-?g?=7@AsJ6Of6M(canTx~3*RNj>4;`Sg9BH82t*z~T z_zc(r{&*t=v||<IE!NfIjbuqrnKIfbfo`~;k2^!?|-fc*S?FE6z3te?1K-o3lJyd{B(#b!GBmc^XS;k;lditFLXJE2W zrHhg0%*@P`l)|ESGY3~zRu+O#o)?Jv|Gq%iJn+O*flGbWeky;otRWn>gzuD~eDq(L^ zdQug4dNqw94-XHM+r2eaVgHoK$Y(@E(o~y^ixE*#Mt0@Ao-_}zu;|>5kB>8pyr8^H zOiaL>*Gr>>N$s7fDgBH{|4pVG%G*9T7^>MHl>e%XYKlG;IRcHzaRP1?UF04jA|jxp z9ZW4)SiII|D%7bnyEzdt90YL+xbU}c-_mDJWl=o|KV73&fUNt~meWyDiDw#wLGPl_E_4UI;ImP7Y=#@iVd=!Oqdwd@gl(FK(QVVf%%l2Ju zjuj12D@aQ}gsWs_0g)0C5&{rML(@Dq2H9WgaNy|QDekv-laOd#XbF;ruG^T1!2Hos zeZq4#+_KAsG@P8Ayu8G^dG?~A?8(W=OI5bEwi6Q*y#wc3;wq{J!*5X(@T&O}_X_gD z;q~n7?AkNrVby16eAmQxva&7j82|p>Y|bzWhQmvxyvKXX(9zV?R8LROB(3O2ax&=j z-&tShaoQTMs;UAlsy=b(C~?dF%Ai`vjfmBB5CglN+Lh;3A2#DWd&aKc{J_=97QXVR z++XfN5ZZ&o3@6uCDq=W1%!fiN$)h7ASGz{kgAU)6Fd{ zJfHrQl$NdwM>YqspEQ5NeWT6iE%gNTXsK|eB48o=5{D!ZD_7T=AW{oZyxT0(r^!my zg&S~w>FMsK=@CEB^8X4C0X_BrjexQ@aa{wrNW32bBC{H~D=sn%W#z~q($@lurez7l z*#tloWw4~aeE9-uTN0J1ub^>bIjA1&?E#wDhA(DNx7e5;Upi)FX4dphp|5%+hp)wj zHFI%ub2BnxI_{!4*3S=OvqH{eH7zYIF|j*v*MuK2F_Ls~W6$0K$kmTlr8Y$L;K>uGo>mtS^=)wk277ElOPft%L zN{ttjF%$j3^wNZt7T%BbfPe?Ox`?GELw5JS74&`#AcH3Dj}yIXYiqHoCB_TwAOQRx zK78o>;&%T1x(O6D{eObtg_keeGhXE>q*#8(y+m6xCj-+#0F&XebI;p%X90(tC2oUV z=RA*Tfo1~;L%?KQUC%)%UFPu&kQL~qbL_1r;2U zve(zwSEx1*X>M+AZ3u|2;WFxRDia!0c{0dg`A7Y-Me=IMge1T8ZE1+ zqO#KYu@*oiNCJT(a|1IlWPtr=8In&(m!bYDT3X*feT?qt?EIB=+cK)U@J`ZxwbM4S z2YN#2v%j19!Xvrlr6VV3XD$_TgXH!OYrX{h%v8=m4$kv4D}UpC1^~NX5eh zy#HhkW~eSNFTd+vMd+9;KLt_Jt^lt9us#h0n2BD1pSrrbm$9g8x#1EK@mSCDduu3S z`sQoYI;WosOyQ5*Gz_%&_R=nk`)i<1T^Jb?mijx!t93+>6m61uv67Si?1+#hy4I^W)R#uZ`KWDGZ!otF@w6vrZn>DVdq@>^DJ${Op z4VvFZ;lOyg=4SZcog^y z_k$=VJ6p>0v#Dv$RnP}jEW6fF8#Hb~;0^#tWCFb+DAsBQ1_e<-cZKPz1gxo#Z2Nx- z$FG%)3S(JYBf&u0g5L*iAk0)$REMkMo?bO&W$aMwhL+SDKG)M>8C^ZVhqrFe@bCl< z`$a*4K9}zYx7n|*c#ThBFyu~3E)fVv!QIb4)&d1|Vrtr&C@s)ZvZtm*|0Dol?ywKI zM5cD@U~JDe5YYPTB*0cKFnfX{IVg#R6$%Dy%vvCBCp&c}15~x`g)BhtSXfx>z%e4_ za<1wn8B#uC#4!@Bm3>G|{Lf`YTo;eHagQBXjdlqMMp#_%eYvgCl6$M4Q03%Cu| z-5MY?6P>VJD#MU6zCp6k)34W8G9&=x?x{wWn~xoKMJFXOR^D!i`)Mq9&iuwIUR=oI z4mm)y&82)q^lytUr;GF#2>{giqY4XxIQb1KlSW^Epv>xiXrE105Q-@LrW;vJ=VLmQ zITD1tckkX{GZ=vr6%pw!sBqrXFSh{1M>!J-547%|?^#b*VT07kX(1ci^7{6|$xXSj znR9;CpY=_rSyM-)#h3%q0;xk;(***@`9|O2VP)_5;laT{Fe)@0gml^%Ue7Cn1HY`R zuTS-H1PFnmqFxAre{zf>O7euBaH18?!310ne|w%jL+MMLd!?1)F?0OuipQrrp4OH3 z!!Uo^+uJWEe*gX*9C9oM*_*`2#m;_MUzzZu%W3J{Hd0AZkr%S_o{bXLVB}s20IH*- zgBuT+#d%mrxSTr!xfabdEXXg|N0I_NO5e3yz~$HKTiczs_}9M3(YuDhiY^z2>!|54 zFxA%;dR!ll3s`}gmgKaEy>wn`$G&nrKS37VOI9+ftgTI4ie7Sntw6XvSP5X-c%Dc@{PH&CQi{Z;7>iZ|wM&nl!3$taUMo^1;> zcX{IfPwR3DA(v!g9VH0tiR53gEw`=qe!Dv358oH;tA)N}hb!g38)1yX#cgWNcx2VZ zkRTDZK0Bhg)rsd}0J6FBFvpew z1rz}6`}f^pl%o6s0&V~l!4Msf{mNYTUp7uM^nAk?I1jQk78_d8?}PChH}a3_UODQt zf0hmwq|xb~{}zFGc`yRHpquAxJG+N(C!cM6oKd_g_<>kl9}`?&uig}o(XklMWq`#Z z1!LY?6djb=%r6>7SF}%v{1WI*sc%^N1!h@0ID_8BFcnx;++iYX7?8?t@!Y9#9ANw0 z9fs8Vl{PkYb~Bucdp!XfWZ>pa|DVpu{N6mXI#{kam!1xXw%9Jbbzyz$nVZqBp7&M3O@>OTy(wzCYf3m?!bG`ukXS0ZXq0NPzyGUFt0GN(L4ou z_SoiIekl7ZF;h&5oUx}K8Aj8%B052$|DE1hl`vqgOS3q=e1Tp3&Vv8dDB19Gtz8z= zPi81Vnm>qTm{th?do)qc2j@}*qhNUFD^r>kW|fP#t$cuKS5>FpC~+a0$=`{c_A(EdmEx~#@Tm#&pkUyC|!scM2LVz(~W+#!r ze(c|UcC}$A{@iDnV-y(wj_!1&OdbhuKeU|_h0k?LXgjEF9BDTp!Yq=1uJU&a=l_}6xu;FB)YhPo5$;Xe zU$U1pJ*etIDi}8g<<~ir8dD*f`+R+`FffdTdV31@_Tocxhr_3*g^xoRK*h7?9~Pql z(De-1zVBfmL4c=1j*@6U1Jn1m^i43 zV$-!2yNqDsY2%Inl}+YZ_q%>(iMyViZ! z%x9>&qn9t2$J6Z^yHBiIPMcj24n823V|Ux4KrMUaA5HT@n(_PQ&)vg1|EN&4H97Im zGpbdy|ND-?bTvlgXLn*|&JX|5zw&32Ez1k+^^(#3tvOWs&8^oQ24ha zOCQCA5nJU%2QBr}Li-yf(7Ooa9dzHctWoZ2=PhP)ucP){z5YL(y>~p-;rl%Q*mdOn|bGT_MY9(M-igLe+Jq%_a}__E9jn+fc73*q$Un%fcLKK`x? z{0SjOC?Su__$ZRu^dk^&h}JzBKwv>IF?0C=NWlv?Esbw(6WtZc)m1@!ams%_)p6k2 zRM1DE?#Adgz-2m*l;#BE6tMn1OV8Wl&kY+)<0? zfl;Znk+^4U%ZOpF&g1#VaoY(v1@yWTWFB zjJwQdM0QvXhXDGBEfByZd!5ArRT+0<+@@$s*9&CI8x;!g;7J<#b&+LN%haV(rEBPh z^!+{d9SRK&li=Zaid=_%Uj|OZUlvwY$S;`h-c4DaO|peEsDaC-S^17x2-Y+4KuT=3 z%dkXneMwyP?75sw?W(DOnP=BPOIv}8A7b*-$Mf` z>pFB_S6moV7s(Bs6kNMk#9y3F=N52|Tk&3;WvuL;`6Lh{4CJeT7%`(VUy+SNc6e=` zPw|UpzRsnt@L2I~S2BtWU=r3z<{8oiUy{o|-Dt#*a32 ziK>?{_J;(P<#-l`v|*&x-J9LDQQVb6?|_FoQzn^fJns5bowW=R_hUYd%KxAV#x>WbCPTMqFe>pD8(zcj^A&4tRVQ0>I-ht!Akae?%uGd~y`6i7m+2 z9DQQ_5Nn%Hcz;zF)32N%bGeCh%OSyb=U!hBTJx(7KGx!?(o=d}(6d{myiM%ShVz=qFN74TzEIbV>y^ef_q5McXQITx{@(QLCvR`cBCT?62nBLCLaX$& zu$ofWTYm-Eb8Y<&pXD%m>3&US#w6s1mAt2s9gilHfhSVsUdwj6ppiAoc#-4hPt9{) z6TGNdg9~q_nr1pGUU3w*YN&nO+p;ax4EDKR%y$+N=de|E^qcjuaC;#sr9@iMf$rw9 zklDElD94_pGb)P8rq(I5-8y20&a*6;&)M~wzY#wJaKu%&CeAMV7U+M?gc zQyc5xC=q_;(x)6)v(=>OQ-T#@ zZcn;tiN=(Eb(073is}shnYb5BqP|W=TH&(fQjq`4i}u2P{pG$R1#7=mpa;BN@ow&c{@+4Yc(^YCX`C*ha(f9zGU$hE1ITOfrQGL zSy3Q+ccKKvEHX7b1G}~gf;)K(y5kEAgC*9JAO%E0P9BkkwXH0!UnVSBLNxO4ohX(| z$Q9$jHL}$El?sef{4LDJCgR?sq_zq=H$Xk9H-t&aU-eR^pKx@~*Sd#i0g$j~0DuJT z?V20WxQN3k%E?oX)KQ7m`xEo7Inv(5haI~tt2&J&U!UDX&AK0y)uJRs__pJ={T^VR zJ{5VQ949>^s85|OlhT$?PR{3SedRE_xl_H4JV8Xfy0~~#JX2AJ@Wii~?$pZ;3gCX? z4n-z^Uy1Ih6Z!f$FVSNwk0&jy&T3i|&JwFQDw!qX`pSh)xKQNw#Yh&(#0nd!8c7^P z!tRyVxP0|_oU@{`)5DE&iG%xL6&lY@BbFfysic$F5B&mNN%3w1&cv7rvmqJ;R(67WTzg*|z@)I&*-8Tcpt@=<_wcIQ8#tJG`p^OP3h*hXK7m>1_my)nHo&WNN$8{Y_kub7wQlZfH zuy|Jr`juX6)SPvvcG<0tDPnagmdv5?+vHcM(3MasJ2b$)|yDUEmVt@AKutMhi>lI=YoTSeZ0MyGqkyd zJxyNnlI5Sb&i5Lv zUMshkyT1SJKt)sUoY)@HEga)b4r~bS9otWv=l$2)jE9h6=1WiLby;Hr)5ej59$Ujb z%ePrL#g5?<1JT@B-QElwznLuA!l7A9w*P+L@2zmwIWvS4)~38Nmfa(Ze`7 zAQyPO8KExxw*}D=)~)OmF`}hGgi_tBExsyq6`wX2$}k1h1kD#>yl+g&?bni>p*N;5 z0E#-bQ8y=k!`Xh%ps@QUEewb`lua#-qIf$&J?`L%!+cR+Y+GE?SHia!>{Bv(h0!eP zh?l+mnQ+b%KaP<4cQOFbK0d3_bLJU$Suhy*4}Se#zo}1;V3NPDP1hkVQZ*BBkz&%= zEipAamK|1&1Xb53@K9Eg*xznhc84)BrPnE~0d-C>%lxNl;t*Gb3R1w>nFsRq-yzyw zjrnVLl*T_kn|Yj6SN0J}KZB61@c95NmF~kcic&@As`>TN&rC0HvbXCuiE@-@h-_9D zhd*%#SMybk96{BKzBk?*eRy9g@*EcXX>8+O`0OEmQD1^7syuF%L%`S%#2@O`mhkEP zw{;BQAX6XlIXvGWynd(ijq4#+UGUMVo9`(`bMfq7I3wVh9gyH;3hrOnG9F;Ow02hP z-rn8;p{mGk4y!kBc)I+nSN*^~dhe%Ie^R5)njmta%G4w(Kf)+WD#EhsJ>TpH>{SiF z9;Ii;Ue~|9bjaQoZF{GtjAUs0fO|$Cx!8ws8_58|_kSohzPV3)oxgZ=5B)!eFm5A5 zWT&{dnLs4mEj}}iD<16~UGfeJ>TR4jDQ*rO^De0R?eiM(6Rj(?6!;-Qy=QBR>-pw+ zrIBzXSGylYN{-}|_uJW@&a(zg+fcg_8 z+L#02_5ZwCIDbd6EX8LS?UVvk_e>L|JC=SKa~G4ce=xd@X5yKOyl+wL#K@cyLbizx z>V~I?vW7@zY@BL$CL76n-9(&OMSY|gAoAFZ6G--+^s;XPXq8zx$ncepnjQWa4fJ6k zqf{c)F$gy|r!NgLC;n36!HD&7XwBUdLDZ--uaUoY zhlBqnv+b)H>KPs5xx~%!oS79Fv7zoHs{9c@ja=Jphgm8RINi@wSe#HJt?TLy(L0ae zyrkip@$DZ--6z!4VFVCu!JprjrfXC`EB)1y`MIrK|EkhJBWdGT#d_TvWLjF|tmQVM zRq9>g&b|J!p@-}ozx@>`Ji!@Tm5=pLcOs~)SdLvLZfZ}9HfztVe2Kaf$o>zv?seMj zqnM!^XK13`f}?K7Pok$Qidy5+ptf{XDA@XsRn^mpCH>z~2A3119DS@;uHV#f^zsuN z7UZEc>y*E~&A{d-@LOnXebNVF=2{m}g|em}qLAzE)7b?o4XFP$oWBU6Z3`aucrP}u zJOv6_>@Wz6-YP z_IIDgU>YF z2GT2vYM!)Ml4fkB46h%h^&)b~y5*Vj0?o}TZpJQ9{YKR{`+Vt>&F6?0vbnUlRYQNCL|7~SJ5 zV$9vRoO033o+TL<*?8*P(n&yWhT}$LP$rXin_xAwY_aY;92R5J5HeCsS7?(yO|}l- zm?)h`khs<$=u4}^eY#AnCUUYjx%o!L$F)f`9}Mjqm67@lqz@#seYI|pn_VAJQ$?Mz zwU!$h`TO8-;KB2VrGIM}nDP;!&KtWjf+=;~iE3_7>Q$~;TSoZlqrSKx_wog>%#0oC zE#*#JX5s8J12ZnW8R#F3GVRgJV?4XW@*>V&X4MiX?;a-!AMX9_}xx*twlQW9u;8DRHuOs5h6EW{m$9@d>N1 z*hE&xRtu?qG3CMl-0_!06xO>(K4H*|Xxok4d6g2_p~lSW=(24#z=#sf6#8z%b4TZd z@ZrGqu2>heEH=;+o63L6Dy@HfR+&*HpM1V_c~*A&PkAgU5r1a;K7MgOt*AS zcvV(+uHPJa(tCCKyjy;+e4fyaJ#VN`@rK1lPjsvF5T|)z;$T7Z<6e#NV&-2a{K7Jr z)AB!Z&k<6??E_~k-XiSnyg9xvv}~OoP9Ysl^~njv}FqmCDw0Pe?g__P-c{S zks^BGSpFHZ@3Pv$-oOBe7T-JmlGwWU+{TUF!JnoK3s**NLmm14FT^`VCN_Xtf}oIu zP9moPv)9L4bkT*90|)n=1{XvWE|o`d-zo?p71TM-wtT%Z328xvOuc7E|0FEZM?qSE z4!@(uzWAN_%R6Kb^cddKirXQ|Qk!LIF_Pcv2*m~bS}vYA;gBAlZhJ~mDbJ#8yFpED zmGRt`uI7KM$)&LC?;ZcLLn_}_+%>mwX7aG;`>zDWy{=ALteGMhFc-U=|t1co3_hWI~mv{D6QWraEOY|*I+_)iU z>fWD{qm0d5wr}|@Dza~zSZ)!WPeo%D!k2#POLAgf5n*lQ!&3QUhgldz(c}6{>gZE> zZf_;z>K^h?l&iS~f33g}=WZeo6mnaY9yh-}{MCy3S^Sf9}0H1hXp&&DBJPoo(p+ru(osEi?1!c`cIxbDY#ru z3*ah13jj<8I_`j<0pL`f0vxms!yM-Vk~}_4brebdI860wr2ap?Et4nkZHJLg%jZYa zdj}DIqw!7uDa_sGw@LqqYwTjj8E$6LckXVjAnoSwWfp-hfd|MCbDLbww+WpbNqBl&z>1!SaEXCv5xtXruhz>nTG|W@N%+$+_M)#35yXPj7vFh)>UQ zRa=F2;~K-qX~G`{f1Xq1)=^>;ImDAiiJ5SesQ%4yL8|cqC@@g>;qdjP0#)c{14;wX z0FvMC-(qA;1z0Nu`H&>4f5^n}#2ypK=K-{C1&I1num112 z@^H$n&V?{ch-y(kzo#(Dm($W=F0ZgY$|`BsRK+zOu*ywM659))!l9J$`dG`UH3`Ww zDc5;9y|!eUt)L5*cE#+xZ{qX3W61&PQ~>gxz+B7FF$}0x@zyZ=Dc-mvs5##u>ffIw zK^E84^7~%$zrFlUHj(gsb@@>~W!>v*&HB7y@4EkHzJqeXXEgKm?ERn16C?372NBx& zB_)jNe?w7k(Ba)jaPfPAgOQwyi;r@bFF;g;%y*a;r{n4<9Nf|pcdZNii3`pAssh7H}&!CxjczU0qBy8sU z)&QT%EfflPK{)1jqI8Ns0-6eD6-GCjc08YWuMi4~F9NKWD;RlsuTGwX8I{=1`q;U9 zb>T*lRq|=&Uzj<58#%|4xNG5Sj!pf#GgPE#4j#p{7y&>;CVm8cRt8qq)+CgGO z@nrL_s$aX}TNAMwQPX?LKl+CVGqZ1_+{>5ln#V21++G2A?;2I4Yx*koL#q*tkN`mO zfTDcxKvufROamZXg^L&fTsKXF&WTs4YI1V&&eqoNnwsw?E?P*w_we9gXCLF-bBbr9 zhqXiSkP8R{fL&LR`ubd4JmQ!VlqH(*f}_;X9U}?0pdIi=d+Yf;Q#T_l)-l9 zNEDPG2?$L=aI)JZg0U|6uM$qc<uzcjjLBv$k-slGR)fMKR2q}I(^|nh)3Sx z@3}_+F6!|{w122A*4^08m44z^6 zAqc!i?CH}Xs$=f3u82>cuKdn`hZqys5@6)0e<&7YWHbY24+syMXDuvWym&F?hS%~w z1Y8{>C`kPN-QBs@vhgw%6%_?V^0b_ahK4_uEM+f`hEVzhe&8uo@$vE3(|DSp;Ogw` zoKLlV69NquOH)1#AW{62oo;Mu0;i*vVjUeBIeYeOzLO%j5~x34At!&f;Ex6V3VuLq z4FI=?-OP98<>ao2)8ugqg%FX&(H+wyL)22Bj=^0hEq;g}~$`6Ib$Anu+$eHsYCcbJZ3hV=_I z*pE@32qrv%UjYQt5@u#uVPO>Innx>x;YV4Snhp;GgI5Ori7E?HQmVF4N@ixPqkk%c zpvEmKCKgPBUw6gVL8sr(&(9WxQ;?VEL@cPe0V;I;{ZH#j>5>~@uZFV_4hSNA;f8?X zV%{B*Ho%|5!q8uH_w_vwp&F3*PwPU&Wn?y%m+vt$-eSU!z~II86b$G6`}ZF_=ukcv z5B)nqK{Xo&Iy%p$PA(uIw*d&2=ID+0n3&4T%c<#>WdZUI95m~&dO+XGJb<$8}Ho`O^X}HvonJBurx; zyS@Nh1PB4bhd%b^HXq;L>T25Cx8L6{mwpJ=hL0ya6+4DsFW)`ySFHkIIEakcZy?7W zu&!_PZSYnjpcDnM^YEc|X0_s%3##=*_!le5;ro5pj){Sv)MBx*=@fXMH6Q>;I=$v8 zuY@BvNJ;s0ICbAkcpa8?tgLb`J73x&hZ`?`JZvceGTWnX8eZ<9>W6bfkrNLgaq8I|rp-oZgM%Y_nhGG&z-yGtRRj?8 zU>E>jK)9IsC~F;iA`uiDE8@Jl2>htrig5R&E$`!+-Fh91H?CF#$T{kH7-3iiT9i8Jv>6}t$>;rpKhK-j_FzPMG+m4+r3 zoThQl7YLvs=RUfYADXQCA_WBn5N7zXz@~c(1c0wQf4>9=kK%ANSFhGvq<6&wG8;hB z&iEhup18EW-rCe;9gS1f&{!BA{^fjDUk1nuNTd_Q-zNlAKy~w#z^Fodg5V7W?A^t0 z+!S?+&RDbmIGY>)2zH^*h4?I|lJBOVU{s0ZFms$s(Ic31Awj`!W%zx@-CFw|C+{&% zre!v6-NvubygM>K0mHjb=AkEvh#-ppGb=9%L=W|1i(vpA)}Orv)17?x(B8b~nF?z( zoUnHN4Q_jdOFXR=%tN7sMRy8xRC06$DDLBiLvEakDYy(&YZ{@9q=4OB)+@!%rjIy$Ccbe&(H`?2bdf3tCDMnq2CF=3F zNoNO5WU@*PGWgd6I2Il%PXrU7gn_SwP{R0*v?hScD#oNrbCYb&V!V z<3MJu@fR(6jL-3Hwd-2tBeLkUaB4gFk7t(r(f;H%IcYJ$Ml4Y;g~(zEZ4GC?&Mz#AR6M?W9^X@e!Aq^NTF6 zyJl8$q(3v6Qb?R-P{;Aw^#lq%X4y&g?`D4O)K@=LqPYe`JO@vp@5^wRzH>!vAn(nb zDlkZJoE8|L&z9bWVk($$C<->CVPJW@k(U4x!|vW*1JUFX=KBu%F3sAT($aP*p`~{b zR9P)tdGWQ}PsGwwSI`|5sfwBC7DTurC0h}-oJ@50xz9ASA=w9F1-O(R20&jht5N;X zNbh!W@XNZSRjS@9hqV6M%QaJI3+@A(?xZj9rXcxTj$1b+2p_tG5)gTH0VywJ^VD`G zZM`hJS5h-&t=B4&T3O{4?MA9t-dfeKSjpKIwmmb&(WD4Pda|jIwlSp&;}WGRF~=>> z?A7J>JKf)X3f@MlUpsuP@8f6ApO<{`;xw14arTyaQ~84HE4L_S!M)Ej9M2Ie zW% zw&YeNB|=4fyM=VTmuyj zzOD}Tki|HSKU*IeJF84*ZT?`)CesV&*EaJod`T=A)(3tfd5&8H`i_6X>&9pNy=-uw z`e{;@vc@%N8B(@pZ7r}h0c%9_j=`Jf4;^p_IeDA-_4e}6(t0>saARFoS*XJwb=IGmnNeLmZ*;U!q@U>N!T!t9;PnhL;9QI0$IHL| zI`i;8YkQcp_u=ekv|P4>22|Hz_(OvDGaN&~5{%T*-X8AY@y-C}DQABvRo0sCyktLa z>!2a=rDzo)+P+Xuvf>vl%G%x?k^4#o_Q8(3LS6m=Wl2?b=~6`9+mmSlW!pJ3 z?x>@Iu*d*q1i~qtzLY!cVJn66QUg)lI)*DCUAMnGxS}`k_t?0nnNi2H@}GMjjL<3w z^DpDSXcdZLj8^Zk6f7@)e2F1+o)vPG9^y8&eZ#K3{Pyo$!Ha?&b?W4~9KggbH&(ecG=$~M9VQjKn)H{~ znlQK-Zj4WDTJJ%E5^J%{h0@o^f={`(TK-x031I9y3Mvbnk6g8=cYP{>;UqU<;UVX&L$0M~+QR zN%DwONAVOk$Veu2svmkRU700X`E_oY4_>!KCAIcKIH{BAjce4{k}YcWU+na&$zA?s zgZYIpk1-oC?bi@GNS?W5{@&TU=&)d;|kx`bu(w?!DnB=ofIcD>s3$wwU@b}2_Bzcg zH8Esr{E3LMXzR8}SLl%$fc6qiZ`sq0qayyfID=J){l$O(+S1{!Kp@5d|Jd1soGr+( zraEWU5(NaWR(HdGfzKY}KpALpb(Q(vz1vKSn3*fqujdbfw$S+Oink6ZZXsb|!6f=J zKnVaoO9PQ}*w%-mt)|8XvO0+uFY3MXlD~higiY-m2#p@bH+Sm5pdeCG(#OJ_WN;N$ zP~zB$fByT14GM}-e+VX#5>-^}fh$((z35jkv=u2eA}PMh{DVOu%RTSR=j<8@VQ~1| zK?d&jf|E!aHZt_*&->e6obPbLwy$Kg_1e=4O1qWH7Zui!v$1m2$ZvzDSlM~|y^K|} zT^ko?Dk|TYANnT>BR;)uomtd8dfK@N;HUuoe)-s3E8?<b#3osbdhCz+!v3w9 zaT{QfF!Ii|8tg8 z4`(zeKH^hO58#{si%w*35U5#1o)Nl{b3Q!q25-uqm07U{MCq^u4TCqrpEz8L*17$b z)c+3FzNOl&*|sZEaTxbAs!yq1=-*^J3U#eB$kf!-u@iryao$f0jWX#q^vgAT^Q;l& zUls%HTHL?zuL~`)+0F{3wcSJ)mQz}Wd$(k`8teUx;Ma6o5!Ht9;mDC*)GYSeB6fyH z5_S{S!0OAC>lmk4*~?i9>aATDIzhIT#%@h}+gWGlGe`Fv85(CMkTf9J?Sq}!@)}3h zGuqC_^{2H2%iY<3q;4yxP;Dn;yK_QezMV=TIcqz;!M}_A+XQQtwPt$2KacrIi}gE* zR&3BEQO&NnC7&(B($lmsy1Go3xB3GW+OOv31*z2*o8pFGVtJtg6K3+4{cJ8{nl;b7 zr&Uyg_3Fa%qluy#gO;Sbu#YEuU(|%Q$1-NW8hx2Yo?+{B9!|q8xzsWdlAzLU^77?M z9<wJNO=OzrEalA8D(FcH1avTYVAO zVKhCL5wq)Jo2)auz_4c37pu^3XVfy9BvZDPbx`ouDeXb9RFhVEj8dxhK}wN`ho4KP zFy&zyLJ;B6cn>6&r2AzI*v|KxmhbnRE_cOFqA{;Q}5>G0oz!P~>VX7OLU4(#P-IJ5OpmH92D=_qqUg<3OQ*YmTgx}oJF%U)PP%WXsYps+uiZ$Y(Aa#jgDrT3 zzWgfRFuhhr-~v`^Ph)3l@j>!1@`2ZCRjGbDQv9`Dd_hoa7^+L?VvVxrJVTltV^8ee zdz$7pp4ioTj;)1fX3vSgQCXVK9r+C5Uq-nkpeK(YlcPrld(!R=Zf!bk$GOc} z+QvoRa4*1}+snE;vAgE@*xN%Sm20Vw8fRahKUy=p`Y9hBkXr7L)S<1~33j0StadgR zm++T=?C%L8szr_KECR-0b6Or@wWsHD>OHgW2 z?Ht+6FPnk`370=NTqJ{=Pl==;u-j>{39l{u8>ckTu_4WX3W0LRaVSlE`1o-IjAb`D z_2sl4>y7M~J?%9)*Of6NSoiK~b^FRfI;K?xrRc7!jfGlbYsc!O6cVyhhLIBSv-YzF z;meWfs&74sqov+ej$z)$a7`+yYK|inR??+Zzt5`$U7aC2=C~J8mq}tQy&;^y^EyAr z(eK-=Z=xe2&Qw6oBY!iipDwBEO8|F%|J2-rDv4~)Qj6{qa~B%F!xVw_@sR|;Ry#W! z_?_VBHn&=(KaQLbTX(Wb*S1$~jr&>gQY#kIPGYvOZZ3uR{)7m&fI_afIeCfj6f z$N||}*fDwU0bzCo$$H~XI$qHMYKp%Q;{OtgM-~=~hkIS+zn6wS!0{U{TP!ireDzrA%Z!mt(KH~IFv*u=Q6>ql{AQ5}93{y@Nc3QBKP z|M{t*DEMMXIO{c?!{GWNd?MW{{`BbwiQgX8h@+6Ca$4)l-!nif2g*7b8QJDSiOjon z^n_CD%;~!QI8u?GJfe#ISUtfuUn?Qg<#*<-Mvgj%JEw&i=zu`kvYtrbEu_I&9GD38 z9Wx~HOenI=0-G#-Z^JT8fl0SLDU))_%+YxD4mzzsdR`#yOmEtSw#XjM2NG*j2^#4c zmQh?w+nk*TDmbD{H)-2Bt?es(L;k%5s)(61J+|z@iTIo}Rhf`Ax1_MQNQDiJ9)0#H zEi%%hH7P9tdm`wVY`tNCG+sKK{S?;r4OVLNd4Xv*KSB=HO{Ds77g0Jf>HFmZjbdxp zGH#QG9B7sMO-*#gerr~+=S`@MtCdt7++TD3k}`kl#A@KiSlR~?bQISNB)*Q>=^>F` zhLK7KSt>;xVuKqchJLsU{K_#6zKMPAo_{2dEb$ml_>ildJ9n<(><%0olrku=Ty)me zKXOdAa+ey`{S|3sp)tFhz|hjOP(0gGiY03*GsrM>XwWMk$H+v~CyjEY8tI!y{klaF+R4*CkqZ1GnbrFv(J`d z^P4{ImC9Q0oat(ctg<0eQ!d`PhB5u#C;6txip_earspz2Y(#Za*|dY#i(lY#(!HL%Se4k%e@ZM?)Mx&Rq{Stm zTZ|8SPi@7P`Z@e^IrN;N%1?9N%RwY>G*L{bKXJh`f0ZIrI$rW?r`olIAiLx5nwlGL zOob>B&iE&XW~O~iiQX(CA3(;mH}zsu&D2u4D>6)9@h(c0(^&3N_gU(*DMb?B4QN`c z)x(l;T?zN+`=P3;O&qECks;-CuX3!zE^gKT7YX+q(#usbA*Zg z+g3ASpE9OyT0QGq;;V}62k$ebR9Uif=d8<1xt}v-bi|FcW~M1s@HeaGD*cH}4m0;_ zZ=#Z0&AtBA>^?JzKlrc&IRP&@h+P2vIqUKTg6oY4fpi8pbH}Y^l0M4|?}9m-<#_mw zMhA>v{b@dn*>r^R&*s%GkA20=RgKU(=LL@PoDwH@wAd#X?>Xskea9`8L^D;coh*Z# zNd^AF#~vixqL)3_+WszMS~2L~qiZ~*j6C_B&l&a;qX(zdb~+K;On-R&1O0d7>08@l z?+%)nF@)MQJcU9L+v-&2j>Mhum-WNOjZ7L!f0 z4UO&dZdEM@N$m&^KFmETTw_@I$Y~$Sg|>@@s^*OJrdwGZ0g9{g+>x9+oP=X^WK)EO))m9ws*}1d7Lnf~A19-4 zOEn^9OG^;(Q0Uw6WNZ%CVLl4q_#yerx2G+%X1q&ZJDn%&VT&bn;Iql698MHv*^;*n z_+#6qNIm9MZep{WxpyghUwtS3K}yq04%*{VCOnYej|oNo|kT$&MAw zBje*@lxE0#K9Cr5_N?DXq0>O&KJFz`csyPYa`DsUdqTUNJwSNXYrgME#dY5Jg+S}j z>6!}L>bxCZ%(=(+HLDt|2WEvw&b{3Zy= z9(!9W))tTU?^2tlIr26HzA66hImP$oNU~l-u6K9REZIkPJ+Jc(3`+TE>?)-{8$#qUsw^^aS z7qfx~)XJrF=!XMJzCFwl%W<@2X+>)5KL?)?>uPA*l$OsVQ4(h^cj<@4#`>kuYlc*-{m=ppkoBZT`2Lg zP9oP5FCV;WJ4mgqPpkdmArjvA;o2`Fz0psz(sX@(R%O}VR*uqFJCT_lYhjOI>K3g;$uIWaTp0d z>#hzeWMo&bJ{RUa32kOT7eJ3 zJP^J)y+QVuho=Zy1G!bQcbS=2mzGj9GHU*ls$2+$o-h7H&r8lA6Z2m$`&B{v1Ydr8}7wQL$c21RxnmsQJ5FT3TA$dO#fN zYZ2VlJX#kZVlhNelzmvDbZ3E*!&B=W#RqQtyPz#3ATkWSa{w_=F$fy8e~0^@7y1f( zE~Om3nG1*7Stzy~-B!78+>-&_0RfSDxD+04-(Ai84Q0b<`+LHN1ri02fwJoUZ5r_@o8wu_%R)Y#3x0~nC z&=6>rd0&OcBCwE$#wW@lJogK|5L>4gmXB! zfEd)0p~Kt~eGOWta1{{fl|ZF^s`n_FyraeO^bar9-kAsE8_)HHQejEBRk`|Pp;W1Yc zkov@8c}yHSOHjd49{lGl_MaVN}jaNx6vi^Y>4Bh%W_&(Zf^h zi$y=7ZK$t@-UuyCYY3SB7cF|XP|(GSh7V($@jp&bmjpdn z*`$*KMNm)M+T1KwangVJ^5W%|!&WeH0O*22Cw=RU2e3T>Jt7t^kZEY{RkI9pUxF74 zBOoBc>u}w@`^ov|BRC#3Nw>+qd^rS-m#2>pm*_h3S%~0V1ZJh$0Te zC!3ml0ypu(Uhe5%pd$-XgL}nSIP_aVL;McSA!;D(CWxN!^G7&aFv5T8!8ic)p##+^ zYXm~XcIJvQz$Eaty9M!lWuk?ahTyw2GzLLttNz3>{CnS>Id=}kJ85Yubs;wNmsm4w zy*aU@o_hDFvrTY4pt11#YAl6cC8v?}Fh%de^0C_eru8Nkn_F-7P z515Lv24Q?}dq^HS_u~^2kPzSG5fL09!na$ayyoWS@ZGCp&2Tb0319AYsK_{Ur#V_6 zto1yBMJQB9Ik>3d3*H1Ab$ai=B2x1Mhbq}@@S=a3cFEk_oc<2JM?L2cYKY&yU0pt= z<_Dr@+jn{R;B>jXy85-^&<=0JARNRBIs`crgxzLfr~^1w$*%|xuqKFfE|cTKk-POX zfEQ$Eo9pS(D|bi&ngN=J0jYNnR~;|UQ^iNZoJ>%{h1EZH2#OgZfcHQbn75@D`lql- zfNWTz4&DeLo*&#I1nPIev9U5ahtMAadcY2VjLbFHllX-|z{>(Y2sX}~|7GV?<@+_) z?Jg}m$M?Sg;h0Qix4_R9#jO@tUf`S7x+i|`y4)+N<_}l&aU!2`*K}Rt%D$2xF8EVZ%=9f~wX^H}}^|u=d;MKF#&5R2%#x0)IC3m37)(s+;dYh^CDVz*XZyqq-_8 zDzMZ}Ikx8bo>Y4I@}}+`9$6hcV7{Fh+C`1k)zvXXRPUm1@`!@^pzXTT(%M>qDpu;n ziy&(sKfgV51%dpqurQ9_Bz6W6{qe%%D-~ZK(d{Yx4h!Cj)6iL;D4x+c`Sx1 zK{^WqVAO9}(H5YV1?ZMM+*!cB9L?j&`T*}sCr%GBL)xR3tMggeVSe#E%q!e5Z^c&x zd4ZbcZ!$bIu&1C1nFQekRJf?hwLr+P{xaU`hR$KdBJCeCG%E&QY6uAOEG6UZ?C3~O zNB6eWt_cQ_jHkhNr+}e?sPMz>+FSr?s@YnY~sjVg_{^&*)^*cn6+WXva z1*_QBC3&r(g?}HcUwAONE_u?c#K~c5K+PO6P%$4tWfr>Sgzu^+$O^FSMS>|4#Cc!dkSm14Jj>`arg|6e1T#50ITe!&$i~!S5fCw zq~56uFZ02TAl2;gJtG2cXC?6M1gaCLerCr1Z!E=s|C~-(sJe5JLk#UfEai0OHi=U9 zL9*lcP@vKB_41!@D;$TudrK=EyS4o}Z>jSSlU1x)xGGBB)K%Hk9_6ULQkBJnmU=t0 zz<~bjHj3P2rMQ9bRJpTY6Ll%DrX%?imIWq}j>{WHohD=(g=X^Su&UY~OnRxGd&74V zRAsmvgH*~IR@r%+oG1KuRFkI{7=YkqGOjb%2ghxBwp7z^S(YRz{T!zIGEGX!u*yw#CL)$lMpU1fDn4vA^Mbnc50^|q3d$rbn z37wkWw}R5(aEU9t*ko#_W2p3LAo)g1&p9GD_Dr6NRiv_Cq>)Ov{C6ecV9hm2rHdNT`?^k2Q(F87W8p6RJ%OskS<|Log`NNY*e zQgtm#LY=i`X#Y&n1T>+kNv6HJi$q?I0wCm>^vjjr*{kV*cp>RqqhGqFR@66El~7d! zi9rqGbF@E>-#{Et7LmX3D2t%4hbW8Ybv(*K*3@cP_AM-zELqFT0@PUb>Ejrym>o%; zdZ~!aTFIUktET9Arcv-G`EL+g$ar$PQc>#th=9E(n!!+Zd--w)Wr1S*1Pej-EflEb z2Wq5~x9v=^w(B2oK`Hm4PIS*qD_PD3?*wT2&u1@#>u-BU{+X7?N>3mIP>D3`&nfvH zsk``~u2myaxl0xgKNOZ#{5e{BZC(dObv=P?VxB?%c?Qa7rR*#=@$k`3I#Z~jM(FMM z(M23vS!yQFG_A!c0(zQ z-Pp5##A;jO?l*Zg_pfn?qhO-iZa{eg&lFGdDkBNCth&>bWt2ZhE9v3a!U$82o?#PZ z%+C1Kf{6>Fmhx>m_6GpgGciqroCM->ihrxb%5DQCd}_^Hi{a* zFGJ1R_ZnXy9ew2^qZ$Jk)hl)$wF554HQsJs32@4QvzOsYnGt$fm!mNm z^690lSH7?=3kD&nS(+WDM})~>c1x24??ee<-^`2Q6L_F=%8hXt9WA?6qDSh;$^n_a>N2@wnXXnAL)MO`5A6gX@o{14jRx*7=iYqZ;k$dOB+>};3~Q{M zNh2hV2rA)c{x-}KD_p#?SAIIzL>w=xDuk5DV8@k_#-O+XlT0c{w>a;~P3zvI{%)CFg zeQu!vz45`iXX*=Wt?FzHmrxHc#->)F^gFJv{(xTQ)pm60%!7hw(e#K3jAPm?ZtUz2 zyvfx15$cR~aKYn?V7=Mc`8<=zFo8ruo}cA@WlX|vIUO;MGSr2)=2|#1`IgLCSVIek z5qV~@+%bk%r{8%vdXxS^ zqLYDE{Y5ep!0~agRV9gyDn}#h;n99z8;FnB($M`7=?s&~;gb41FnG1;_F1zV=!*Zt z*jI-|y>)Hds0gTlpp*(ogVGI3gS2#sfFPYBt)PM+Al)I|-7$!PgmmZ74MPnb-x@sU zdEWQCzCS+v<6MWBncv=Puf5`4_ub9r(!F-#BBCA(^;4_E$?__i zr@}trUU>rc%Amq1b${f^u5PVS{xJtHhp)xwswZ`3xDMrL6}O?Hu>$`esr&0TE98qm zm{sDc`#a;A8tHm1hnIR(xExwu$!jV6CJN<%K9|?N1CKn{)a`rOOCyDdeAnfK{m9X_ z{`0~{I{h!C%2D+7e5<;cdJ3qbAv=Gq5EXRM^J$VQ*iivMr4k&e`T0BC* z5iiU!S-DqT$nCw>YTpv|(uK|E3W}uboHVO-TE@pMalMItX8d*)PPv)XDqE|0G>fes z%2Ez}aTjLu`yUftlpz`0O3D<;B?$4a*VlfBqw@|ETkbN$G~-j`Xk?Pd9HX(fpa60u zhh~j!M~?tjktO{|qtqh3+;z|Uj$L$PXT#GxK6cSL%b|OQPV#wz0f!H@20yMp=bfMo z)6ld(Ql+9uBUqY}+d+!Hap=d8c0UP~i$*-n1!L|>3=E;k?|6H+|vdb5wr3Ugpl z&pwO@UhD-FF>5WUP1Ht3&Fp}7;H5mfyl>*#|MP+TTYKv4vdINRkc4;zLj#-i$|lBp zO`#NRWE##yHaDDPg30;WO9JF8Sv0I3oV!;y8EokCh}=$;<_Bl|Nyyks_mtIJbgXXk zbEd-s?oaAeT+XC+nwFwAt6W6J4nC`r+Rz1s?VJBt#0Jy|U+XgAep9K<_tOVMy{Vz4m%e|iRLJO%U4_KXELNXHyl3tm zO`|BTKn#A)v@z>$i0+9PY^Et8EG<0?(@(^fo;H-uXi^{!V( zk|`!37xDbhy`#>h_}qP6$%ESiGiZx+Jv$ZWpu z^)hY>S(lPa_Zk~?sayKOIW zuAbg=)+$Jg4dGzFq#TiGE{mRqeb@f&zYkl1Fk7V2H(6MJ;aVBJlx$CuewGSJj{F~wcgvgB3M74?R^ zq5Z7a$wPY9?Faj#(DnLq_Y`Z>T!lHKdL$`aAvJ_S!QI8AC@WX8$35xCk{)g2)tno8 ztbN~T9W$ja6pmfm)RUG@QI&qoJWk!YFAab?-8z(=+kpim34Shi|41|8o0orgJ$v8M z;G7gEv0ce}kgPnHslm#)A9`J}x1@`CdJbZi5r{T53*%606(o z5oR9sudtuuUvJa>TJ03Mf;9cTk0_E1=8pY6qbVU8D4HH_nJKs{BC)k?t2Z&S?r1T6 z0>O?8J!OtJj|gKrZ9dBh_3#1iBUxK!q5|Svo2U6^C4nzhpHb8g$S}Xj`_^4|Ze0f= z{$9r=?BooC`9m2AMiiUmPU{^T4gU^Kamim;xQhecqt1Po%`Iz;1iyP8Y@In*DO>ma zt);D*E7(oaGtK=~;DkIUPOQ_?UTWiv{$)uOo05Ex$5XQ!KXM1pkcnb9erEK8c007x zCNdA&1XFJUSGX5lddz?S{= z=>Eo&iV96Pwhmrpk4wXQR(!otC$rH^=z~Rl(>Zz`6rm5qhT~ZXEj?nfBZ6IAH!G%{ z4!|o?Kc&BsdA7A{urg-5jN}PsNMmOg**lTkU>Md-v?$JTy?~x`G^7k_aXMUNNSFQ_f3k<1VM*6 zh?6MN&+J0%kfux-?h=R4np+t38I|Oga*gPTwAU*_Swl@IgQJQS<>Je5HJQpk`?6NA;>EfT!*2+8DoM#Rm?rzvh7n!s zC`aq|_Yem?pQ(0;k6*z&9<&5o69DIQ)&l6I>@;n(J}R;|Slz~#y}dBGzTc!c<<@^W z(Zpcd*FVF*Ecw~??UaKnTgzI>A*u$h{jZnBzf19YqFz@Ci5&-%vIC52TpiJZo;8f1 zz|><9T5O+_GE%H96wqG=I4!ad#5fupmFxO*QyT1}(@9DT>uJM2d2~$uxD=Q}9@RsP zEG4TBvJNgm1)CsK-6FOj4zIuB)-O){>r?;2J-uv-?uppXLF%WFJpX&Dp34U&Ww>bc zxB3QgixRTbt`WT?3m2&EtjcvrB1>;zUJDaO{5p;3>v>LJ31TSyGR;y}oC(dsWM`I^ zFTNiYm_OxBC3kM@PIlNjH#}!)LH}zIr%lw;r^9Yr zsK)uI$iH8nwd+$C=R`;;XzcX9LBgWsMslH4unJ~%Cab9+55~lF7w5KgUU1!%yNQYJ zp2h4kk3-bY>+$!OS|eDS(aB!tk+HqWH(eu4Lu7CC|dzw7cLKt zS-6!NC3ErdmW*3=GN08zKG&H0_pObzhlCZH)UGKrA_46)i|mLNYKae5FbhGIg}Q|v zT|)KPxthBVvNJTlvwFHDv1dZ)!;^A9UiFc;MQ&{2vzXt_I=rgl*?noZHvrkP)&z)1 zrgCzQUms)M#G%vmZf6)zv4sN~jK+F%DC?WuNuQwMAA%J@x%|2p;qmYAkPhdNGr`H2 zY!WQK*U@i3k4Io9>e!!1?V;}WayEZ{Q9!h|%S?-q$wb8-b2n!E;LmjTn+j4yzaf7V zVHFPQ5L?#Hjc9Nr zEBWt3oRd;=E6+eRkaiI@R2idf7k8Wo=dWsvHlt&punD+y+=#wf)>3t@w>pBw0n4?l z1yvKR;#akEtK#zaul3}S_6K3eKLL@#EB?0?#fesk1EhBr}-t8MX* zWd|%)V;1eT0y2!<;vGQRX>Ry2oZGrsJZs~|QKge4aq$YITIaE3)n2WH1^;Av$=EA# zeW*-%DqY1fls&rj^Y8ldvE*u*KEZbeCkiXP^EcKTTD$}1b#95N2(@quCBoJ1Sj4|A z@7iAo1xOzHr{KZr=`As9$ybGv<^>F_z|*ZKpX*(hs5KS%$d%(59JtdxGUrZGPBe2p z*0q*y?4-v`yKWOLdL;0o2-kHa%pZ*ZwJXxh$p>Gkddu-KKFwn9_X{Y4z{{`yefg4k36G;t!%7qxFqO&d?I+Qlx65PfM+bu*4vhh|!?a*>l z$Myot?^8#K*&BZpr>trJ<5643A29+BN;?t}9=E+XEqa9=yS0*O7V;hk4>|UTNOJL8 zX|n9R1Gtfva?4cCr~ZVPHFyOlqR&|VpzvGs$xbaZWvbBtfZ;#uNZeT%KOF@UyVInsxLy>-!PVs^6KieQVpzQgh11OD>#f`X4aq=_2Vf z{M~~Pl%}{|7E6}B!3&8eMfoozr+k=TVyz6_De}l^fm(7Fxb)k zuhL74UsD0Vi`J>GonFM&MVKVJf482W%Q{P(A7k3z=$2;esN<5+8>pf8Wh41+qmdNG zr*T{VlD#XHW#zp+8dYEA<$ox-b2}x0t%nG|s{VO9=Xb^aW1l})Ym0{Wcr$#==^UM3 z?q#s`lpIEUG8~w9v&VDm{+8Zm+*lU(sN8a7+-;~glT8L^v%IN%H2*?I zvsbx-*p0o?4FE-Yg6r?xmj>l?StKi^y6f+wl-iFKgqPBUq2XZ+X~clP1)8KY6vb~;~+cX ztCd~z!Z}#|n$VlUU{51cJbveBnX6V^Kt?7`V0L} z`{vc`=2Xed)3Yzp%7LGnE_w1>;_A_hgxn}>R>Du0Z47un%| zWm{X1+cZ1yIyK?u!{1C!xk_9WL+%xf?ss!M+QLzUW7tFsDX zD^5_0#zF5mq_S`mx%TV3^|NZ)xjp(yg1=meukjEb-jSXyr)NY#o1pABfhp_ctAql6MFcu0mA4Ru>^ANS*54|ohCYi{{ z75x51&5ky>q4D=I3!1r`h+`z-o_YD0?Lzn+)EzR~UowWpQpKO@%=8du2@$dW<1>%E zWz~{LUVOUlw{bOO?pdigQEz8LJ^jkTclOT%K64`fxog9>bx0n4wZL*IyAd$7NC`FY zGAsk1z|P-^vcs3ADVMm8Y>J{Hy)BT0Cu=oIT)EV)>d9ewJHkxz8@-&%za3Xv99K3^ z7uh7{};y@lr_sJO9EkxH&V5NI=OSWR8L>l4ZyS=DI>_02Qi3Zsf`s&!J|Rww6Qu33pqX!xmNm#0T!WEG{XXL}c^gI_$VXp3g- zejoL2+xiavdCT}BsuB&MCx6@0XWn+-Wn?r5SQq>dASJ2XXHA!qXEnW)zde2VfzG&XQSAf%Ua&~5oK8^Z-BjqfB zg-F?_0Z6A{YD!Oi8RK(-P$SkS4FHgVu7K8pjgg=Rt;D6i-0Aagapv34kBYGZxU8m&DPwSKO(Sq+<3ssb%6wAn`L0G zW@L0UO7A*!NNU#8N9Ot&%Kf zyj9159BTt|IBRn-fph210ounFpq)TK(be4@w;X^Qi~tOCxyQ#`wi zJnOpiC$t~nW&r~580W8(k*(x-`2uEe^re^~SeRb`4t&2BpMc<}XwaE6vOY4uC_#2_ zP6G>2ZzvcY2bIcTJnL;BQfgem2a^ZKG(ZGMBvGaJ4;~vvXaKPi?Ct@E7q7cJ#Pux* z%?XSG_s#FJCgb%$FWoXTC8+)3Mk%LC^AxHD?`Az}%zw_RV27rXaTl zP>KK{GQ4=Ykisyq2P>D{2O>#Iy5w%B2Fwp*dmCG~)^r#m+b?5Kbr{?s@}dbRC`*0{ zzHEHSZi9n-sC-SwWbL;P<~gGMzJ2=!Ku36vajaKh?4O0Lt^pttNcq5FV{-!e6BQhq zx3k|{TC#x#2w-ubo@%5U-tzGgtv?lzwtTj|m?OSb{LnCfP_;N;ikV5h;4O$>oQ;kL z_Le=B2Na2qz!W}H2)P5#9Z<7@NH}MHJ`hncGEP5E89762dhmy>-DelWJLK(M2WY4w z4AIikZ?P{iq*JU9L)8p1X!MQXvwjZui$D$BfHr?R_!-OAA?8hfIbLZy<4?uG;V}90 zlgP(w!133%KJ^>mqWw7<28Sg;vDbmEEh!_z#%c(9q5V3Jqri+vBO6V{62YkPWVQmg zU)N>j+q?4zJ1B}JL9J5E$$wReNYC|=5N3Sw8xMjH@Sp6ia_-@u z=gK?}wS#zG!Ziod#*be9A#RG2VX5tD#OUhmzd0{pHyvT)K3LjwZ?dGd)PQA!xH3@0Zv3W=n3t6Z5_ z@TLZ<12^clG!ZPrd=I141MpHwNuPBucEiRmipk=jY!gC*)-T!6wQJ=N4}e=Ph=y4V z$cEO>h#oim`W1Aq6f{i`C&vfgxMaM5rI$fE*x42PDqjNVQq;3YhUa2$g9PBiJK1SZ zqrNX!;JkpBazpo?zjW<+PH;9be!Vn_9U;{eUpd17P8CSM9srqV(p3>B%(Z(Pq~&tH zAa#aiW%(sN_lem}I)G2JKUYhF;-h#lAk<+Y>X0Mk6?Y-pQQE=)1H}vg=LbY-#7ZA} z*bG63&bo}N8;$c2h{>ct9NLkj=?9)rP4BNS3^X8UbOo=%Vwf|XC1<83LIwepeidyF zlUIKY5d%6sb$;gf%Xd>({al^u?PiAjP*=ZzfU&ExYB|vNy5Dd<88|CpNMO^8GtXxKp3@ZDuC}J9>rLiY>aNLvuxzU z`pBkmq^87Pu0!-q`_dOj`%C!|EP8^^zr6(tRS18M)_&m0+72Xm9Phzf;bCL5YIym8WsMVK*cLl-xW3c1xsfU7sR_5&hZTn@gzM>XLCB zb?BF%aC=Y86zaiiLtDPH#vdN1Jkr1y?L^bo{%#(RO{jh0L;R}=@5BxZfoIN zH}Uaz0BCE46wLHXaNE|Od@e#-4cr)P!rSz89mQb}jV<4YP#G+dRPLRUmu=7m|Z3m!T zVKxL#W3=D$(GD`gD_^Tzj`@5g8L!g@)juD-#q%Qe=SmOJ*LdXok{<``&_|7wf#zd1 zdvT6qQ6S7&Xz)8l8VL7tYc)_~7-qFsjCi+M4FP6bG!A#t@!p)C``(NaHn>mBt__S= z&c!cvr?;0Q-5FH&<~x&TbevwfCZ{L3?*q~qV)sZ^BWVkosq*lf_L(nA8Tp$20E;Vz zWfiX2H;!$wJmsUWV;F1pW@D6cxJInKCIkQ%dNk?Bk_QhuY*i<~%37q7t(sr6;aliO zkkMJif)%RsS&6e^HrlL8H3%e%b6&kmvfn>>oX+wXLeyIjh(+i+FT6c_9t2dSEa4Gr zK`&h~7IbH>!7z;JG`yOW8we-AC>?~4NzZ{Iv8uV(f^^;VXQbe8z-3bD;bKuTaMkn7qfcBIbC^gk|N?)&N{`9$N zKlq}Qo=*c@ayOvF3lm^^r?CYNG2nG|ZcXQ#PtDEIw16-S7-GbXP0D8&r7*0gRoFDJ z=h^ioWcU-)r1K@TTG8hclR{|QkDToMTHh%b)^)fA$}EO&ElL0U`IGovM;t2K0*M%Xm1Sj<2)2Ro*R?J?ocpmDaCOmP zMBU*p(-xQ2%DFBX7LM@#JRLssfrrT+bBT|={tQc^nFv=!@B{$Eoenmr)h9V)$=^~u z4F`ne;q!0G@&X)A8`A(@<}~}pn7*R}Mnc2fH)Leoi?apCHNej=WB{7^9!CFDEa6{k};0HkVx!CdbN4wC8xk@4&w!+Y~$Z$qKAsi4zhl9HzLnXn9 zwPXTTIC=7zhz_h!4sX4Vm537fJ?ZKq0I2v7M#!jT!k3YCdh~;kX~6SI8&%oPNGnn} zIl*9+3#wL)vl@J&sGH!?mU8EV!MhJ1c9=-Va+rnBbbdag4v$v7Zo9oa=!J#nzBB5W zYTMq@!ob87hP?DB7xGLYp*k4A!r0!-=^;}u47Wn;&4PVvmYRlDS634tu~};FexMGf zVzu9zSJXhPeaB8UtlOQ$))<3ej1N^qdGV3$wB*!Vr9^OLLhgsV3CCOMB$gGAd!$vH zZ63(Q(k4*1zz86aALCuV{Lne5Ojwb3=OmFK^ezIMd9k{UOl0_?+fFqK;j)xlZuofd z#U8_xVrTRs?6vQfJ;*BqJFoapiOdLK5gh?AJB$lu4*K6C05e{1@F!ske6Z0|Y%FgV z8XG9MFS%%w*@vO=Mi-GD!d9?OH|=u&`+U{#TvgyElmMEzb=?hStHROwwcF8#$3WI= zq){|#OO@Rb zgbb>Y250ZQD+(kRsB+!gfLL>C&#$m+03y-zFq5HO^JX1v)fbhdaM*Y5*U&S_c8KkK z6x4Bj-NIpqlg0h3X2P{ZgXCkm z^>f(wFZ+^$W$2DUC=q;(XHIcxUFWCI_at>X)vMT0x>rP6Jp8x~-jXH2?EOCG0X54l zne;m5UD(0eVX_cTilQqQ(3d?fR%TED}b{)!m98Zgu6*o{@iyigXxs~{QQ~DHm4rv z^z~89M_auG@vd1rYsU-MgOzeyT!( z@#QA}RPsdiIMvbZJD)K^^bg!CL9`JZ_gX_bi z5Cm3|*sw4oJjJEBZ;rqm(hxv43`7|QRsC=*bnQFEm}2kQ;x@QOK7mvVIFAMQrtvqc zLCPC*&8%C(QZQA&B_ZXp6|{xjQlHEdi&FB$U96Vf5WC~mld41uVG3LRAVdh- zG)ktpilMI$ge0%T%i_Y~0)qYdwG}R;Yq1p)VUZolWi_^g^w7h6)PXff4U8%`yzm5J zRwCpi8v+WXeYfoP_0q9Uw;jyV|5=8q8;GYz5@p0HjaqIs?S=orp%cy2^_MuS#3Ho* z6JP0FbLs{}LygrQpq2-6@hs_7j@-6lx$1?JyX#Y`5|<%To4s!OrGd1y&^6+wdBKI1 zV)d9sKlV#>J`mYmyUqHOxQBsb4q`J30)lB;u~!KvUZLX}vykw~O;)S++!oP#hAwUw zS2gzCPy__lez_lp2V?; zNq+CFt-YE53V6IMf1YTAJ!b0p#G~77$9@CM1li@B7DH4N#!o+$8#di6q|S~KU9O~E z*~IL13n}!lc-)AVI~;-OBlRUPZQ8n#{DDE(!6KuEqMlqzi2!yr3!u-2xlT){kwL)G z2X~XjP|5iC^UVNEz+$cqDgBKrlU%;L6T6Gb(nYzP~&D;y5C(3h`gl>=01p?2K%^Iz6&_cmsr-h@Mbx{~(V$Xt{k zd*u0|>l73)9@zt~4`kO}MyniV-TYbo{rpH^gcFKkR#giRrYfWE&Ts?i`=A&&bIbuN zm2a~2>k>S{qN^`HGzN4n^y9TM_T7)r{i$Uzm8|SWBFW?gk@D>p7y=CTr*N9SF_p@# z3jf1gv-U(W6{fiIG>X%JvK=Dtm#ugBUgt1kc!Zxx$-x-w6Z%O=p8K3|q4uk~1nN?_ z8Yi&DEk&f#;SfNq4;m-*3QFx+d`URFITrvkUqusakZFWNcyGKHH&AM(<|Sdrh=dc^ z@3?eu-@oY{@cqLj#Rg@FhYuAvcc&;$Of=Hqrm9=YtH$_orqJ!{P;EoI1Rm^EJT9S?Cs2~ZCIv;vaL2g6fq0ZZAZsRZBbeSlfgjjM5E=t z{)9=%9G7>{@l4gIM?9D z%2=OTpA*867Po=)>TLn%lJ${Xuu5?}&f&k$LZa;XS}VuFJ*Z*&Z4_i@gS+d$DYznD z!AP_a-1<{9k&g_Zpv+VbVQ7j!b^~mX<>?0P{da=|->fC}DB}qou0;)Yy}yE=!x_*1 zN=ZZIumUn1_sv!=i2%|dos@lYl@HG_gXO9eHuD|#2E|!_njV4e8XkF5xSOmt-=d&E z!XjrHr^<0zscLFM(y~LO`=H4>@Xa8+*pXjkS=Sq-~CL^$nqk z0z_sjEM1YLv~+Yg(y!p+#tXVtnVXlC^fD_Crh<=uLDkR#&Rj&dc^gyS$<4wGP5M^}LXQ942`?AX;+BY-_$V)OPacm7x#e#mHi}gV&jX z&t*-Ol!Bi*o0!m+F6Zmk20yKFKdQp;El*{^pj{e4)6U)6ni`;R4-N_W@%{VH7moAoy+z(%OTYFUKRSEkF(EyrE75#9DxSwK&6pDO5nv)doX_;GYk61< z3NGUP-Cg3_w+qYh4c9j}r;a^j<9}Mol$MsBdv_Pe7>f$~?}*;Nv4_u5dZHIZFNrJYynM1`76I`Z7j>tB@zQ00sNY zd9R^ztM>TWo=*ykKVw;Z#=TX=cbPlnkvcVR3X_v59gUiCT%=l(h1s6sjKz%6xXSe;^ov z`rbalpaHkbHsfuQs6miCJ=vYYSJaxLg)5OF8h1cbK9iNNLH-DNda^XlcxhH#QQb9E`T;{JD18NT+k@%mimEN1=g z_|AQwc~slhD@DQhkL7((%urNUuP|;Wx1@ib)d0i9&oW_#RU$i(LMUFyL$Ft?YznFl zjRa6Mr%LG4D}IU7at*1HyN8)M1$`0qWbxT)sQ1_ATdz-w!H}_Z`Yk&U)-CylcsiQ9QB(~^9m9kfGM~oZ_ zFf%jTn?v>Bc?=jb*+ZvDL&cJ?p8@_%!le9fzPz4JLr&4jO1Kf-FlY4~x~&?IKT&t? zC(v6AJQWC`k;lcuo3k`%4Cm+PpPwwYI=6S7A!KM6_EM%T^4E5fRe;=kCBCJ*_Xqwo zL(SP4Mq^wSJ)2^DEnGM%@cyLZP17iRw85vv< zj~(Lgc#?2gDonzT(G5ct)z8n*!rUCTpH>7ewaP_f3yWI)AD8HB%F4o5!yI1FU(Man zY53ud2OOeQC)f}6tX5yfvs|2krI!6DPIa^Mk+sMRn;M9$;B%TY3()Z>Pk0RaN_XoZ z#V$B(IenasZG_}%z%IPR%E5ty;oU(iA}=q%rAdmTgg#VL3+qPxtoB*3YAmEq!Sk~! zLr_onGJCu|Z1t>g;h2Sr$`Ar^s1}=tkAuIE#&rPw7!F(QEY6&(oey3k?5kgym_&w$ z&yJ6Odx_WH+L}77U`nTpa0MhD+@6bVeD?FgT&4E-{C2ZXck`+Uq+8$$u?NOA84usQ zUFQ2f1pd9fB=w#jm-e+TkHredY+Hlp!z$Y;0c4=OCTT|v-S#VF+egi-AC2@ zG4&1K41Xbw-9`$|ASVj$db3oZ-Zw%|w*kTDkMNz9VPi|nHeEqrTye0rE+{IRbia~jD#dvS zab}m^_OvBL5fEF@(u|VY1)lriIGpz6YyQQ{44xmJFT79(skWVX<4A6s7tprU*S~jr z1sn>sp6V1agdNkD=^zRLKZk;F0Q`{)dXd9r+gzI8di-r}?#eGeBDkzgU%arLYa`g%9>D#?;ibc4B*9xpJ+ z8q>5*akA^()4s=Fx0Rf4Aqy>NTH!o_i zDJRQAqCW+a^zn}j#9`B)K7H!Ev&yvRh2Q>QtCZig-_9+ZS=&zEjMKa`Q= z?}vfwg^u^hR|Un7QXvnhX=sQD2t*|$l-r`C?}S4u5E4It{vhkxTR%xd%V(^k-JIRLyzNr(#KkXLH`dp`rfox=;{_8d zM?RPwN6Y4XC*`8TV1YeLRt-&A+S&B71D;ku5n6Npf$@!<`L$;eudcI=>H0REp`(3N{;q(J|? zhn@>Gl;M8aBqX*;lIVl}+^Z>%|{}6(_S) zIoE~4xg~q}{EP>=nCv~SySP|Vrki-cm+%MLVI<|h8XJAg09~NDTlXvgVoAjsXgyV8 z$ohe5;aGOwOe+*n096|!%O5}Q?!L*&#qsaH4b1Pu!^4<#U$0I#3Q-1!{Q0ycqZ0`##nPm!NVPUPN({0yj z_VxRU-mj9y!NKX>6)=uL>tGI0ZY~_4Z*$el0kJOB$~SDi$tG?Gy!VK_IY{Q@8eu6M z#**H2-b}%Aj*wH)vdLwN%g^;;MAviKL4pu5=gb($F_!hia=PIE9xkveE3HEcsqVK#GIm zmq0Lxw25Xz9otX_Tzv`V;wqy9(HJ-assIv!1~XJ~JPVR=@2g{uf5&?iTz653-{5A7 zV&+$^mj?*?{5iZ1K2!K|s5*cz&LemP952L{3susb_Y!x5>FINh3n;xY2u;-td*v z-u7(@@8fjif|eRM-=c+{(C#q9_N0yvn9?X~%q2YXrx%m*EqI_6JhQMcxGDg*ILPdn z(*z@zl%+lmF!#i)BI^=vfy=E(xRiOkz0@D*+*fSeu2fzNdH`h$plSs^ z<=LH>$G!)9*H4Yie98gt>gYG_5m^`9%ph@CT!djFyVR>9H#vbQ?6KD+MRK_w0vw4a z@e+d#ffVu_-sUI}SW{xo{+|yFBuC>ENHlBl^s<0Psq2i`1gg6r4yNvV>gO`ewl9SeUE%~lMp{sBD>{?*RcOgre zkg0_)S26kR#F4TB6i>nrE}YvO1t+AlO-UuM-AVXk^V*O1qmwrusrDc=z9kJ2k4yBOywv3~jgH{;9US7{aqkxT=+BJk9qRb!542o$` zvv7BJ2Y^n`hL7m|f8Vo18)*beW}`oUzIQ28ksg9#gJc`Iko(_v1Z65QL<<{zGnPR6 zH;*%iQz->DTF@@OyK|LRD83DAFE0sS&HmST^}N{)#`lE!Vg&%iDDgqvgpdook8Nrz z_`J`#kJqiK92^`%<;uN$T_cRa*8Dl-Nm|!2B}b1Nj&nNEB2dq3^47{6<`CuVbf|fb1>n zR0=8I>(7kcZEXQ%U71jZXX4}IyLt{YLtYBfZTvk%nlY-~UeX!^lJ%2%6fYj+V01QblGbNmDp!yQp$ zOe`#F>{y~YI-_vj4IQ7sD!{GjbSZh_HsOqj;^|v$tWErYO$Yv)CQBs?{}? zCpqm^!YMi&FrQoXPt?zPekgzp(6=4cjMhoC_~zL8&sJPl-DOtrLcM(X(pK3A@(5u) z?CYu*u^SMKWAJWkwZ4VOsO^9GshRZ>U3%)u#3qj8=dV@);OhLrl zz<~0W->W4caMIS%f%BoIq+|;@Wl2d1ltX4ovw|-!+UR8)7HHBko8r;B5;y;rlsK4W&Xxf3XExjr! z(eRySSAUsifhH5YTn6(#GbS|t8yv&S^}5bYPlLXS{d`A#XQ$2a;a+eZXfUGtZ{508 zR#s-ts4w=3MGpZnWeS0r*%Ew6sCg?)!UHlgG08rC3I(>`{v_#u*JM!LS{>C|`~p`r z)%DAOMqmzufNmJ!=Av2Qy2r}VIwIU6%liD=8B4(+aH<{m$b{TubK#cY;NVbcI>#}F z`H(#%L=HM0Zhvp-p2vVg19e%gvOP>y4wCZ6kI%+<=B1^5O!e4kqUm|CI$UlAs3pMO z%t=ACo>~^u-2UfJN?D|Afh#nW0Si%CQK8TVaS`1R7ayNi*$TpX;v88MKzLHSfdA^r z!u_C(uQqb#nz-25VBlsO_a(%|0XI{KAz&P`fy!;kAvns#a!&IBSjjsmGc6|dMSs7l zygJ1HUdh}o?jgVmWE3X%h3CG8_8jD|O5KKy4}5>UFf9TF#>#}-pdUGN>>_9R&UUcW zjEJlWTzN_tgb+^`pb-dlqX!;PEH$640VwG4IfxP)oztLe-vZkTVAA&X{D~vip5;m| zqpLmf##mas|9lX4q4u}`M!~#O)zNoaFCemQH;*=EWK0Es4wN6DI;IN){(^;Yt7?Kf zJc1~+(xmx8JSgcX#m2?Ck$nBQ0nM)9Vb>NW1?Jy@6zQ?{Mk!sObxC_P+ZtnzhEold zVpUbu9$jUwZ_3XhqhE=HKZMpNP*`au>Hx9Wm&iz39z2

  • 5XC7NyTKxtJHLj`tU< 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 literal 60950 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

    fGj;W= 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 HcAWkXCgytB diff --git a/contracts/docs/plantuml/sonicContracts.puml b/contracts/docs/plantuml/sonicContracts.puml index b57b800b5a..192b3523d4 100644 --- a/contracts/docs/plantuml/sonicContracts.puml +++ b/contracts/docs/plantuml/sonicContracts.puml @@ -44,7 +44,7 @@ object "OSonicVault" as vault <><> #$originColor { } object "OSonicHarvester" as harv <><> #$originColor { - rewards: CRV + rewards: CRV, SWPx } ' Oracle From 83c5c81a7a8f86d9c1549ae8e5282cc2a24b0310 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 11 Mar 2025 14:28:07 +1100 Subject: [PATCH 76/80] Changed mainnet deploy number after merge with master --- .../{125_oeth_vault_upgrade.js => 126_oeth_vault_upgrade.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contracts/deploy/mainnet/{125_oeth_vault_upgrade.js => 126_oeth_vault_upgrade.js} (97%) diff --git a/contracts/deploy/mainnet/125_oeth_vault_upgrade.js b/contracts/deploy/mainnet/126_oeth_vault_upgrade.js similarity index 97% rename from contracts/deploy/mainnet/125_oeth_vault_upgrade.js rename to contracts/deploy/mainnet/126_oeth_vault_upgrade.js index 3d345ad9ea..9009cafaa4 100644 --- a/contracts/deploy/mainnet/125_oeth_vault_upgrade.js +++ b/contracts/deploy/mainnet/126_oeth_vault_upgrade.js @@ -3,7 +3,7 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); module.exports = deploymentWithGovernanceProposal( { - deployName: "125_oeth_vault_upgrade", + deployName: "126_oeth_vault_upgrade", forceDeploy: false, //forceSkip: true, reduceQueueTime: true, From c606558537b1b745ab2c46a5209c49d9a7747c05 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 11 Mar 2025 14:32:52 +1100 Subject: [PATCH 77/80] Removed Sonic Curve AMO --- contracts/contracts/proxies/SonicProxies.sol | 7 - .../sonic/SonicCurveAMOStrategy.sol | 30 - contracts/deploy/sonic/014_curve_amo.js | 102 --- .../docs/SonicCurveAMOStrategyHierarchy.svg | 74 -- .../docs/SonicCurveAMOStrategySquashed.svg | 119 --- .../docs/SonicCurveAMOStrategyStorage.svg | 129 --- contracts/docs/generate.sh | 4 - contracts/docs/plantuml/sonicContracts.png | Bin 61878 -> 53548 bytes contracts/docs/plantuml/sonicContracts.puml | 22 +- .../sonic/curve-amo.sonic.fork-test.js | 765 ------------------ 10 files changed, 1 insertion(+), 1251 deletions(-) delete mode 100644 contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol delete mode 100644 contracts/deploy/sonic/014_curve_amo.js delete mode 100644 contracts/docs/SonicCurveAMOStrategyHierarchy.svg delete mode 100644 contracts/docs/SonicCurveAMOStrategySquashed.svg delete mode 100644 contracts/docs/SonicCurveAMOStrategyStorage.svg delete mode 100644 contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js diff --git a/contracts/contracts/proxies/SonicProxies.sol b/contracts/contracts/proxies/SonicProxies.sol index d59d3a0477..da175c34d0 100644 --- a/contracts/contracts/proxies/SonicProxies.sol +++ b/contracts/contracts/proxies/SonicProxies.sol @@ -45,13 +45,6 @@ contract OSonicHarvesterProxy is InitializeGovernedUpgradeabilityProxy { } -/** - * @notice SonicCurveAMOStrategyProxy delegates calls to a SonicCurveAMOStrategy implementation - */ -contract SonicCurveAMOStrategyProxy is InitializeGovernedUpgradeabilityProxy { - -} - /** * @notice SonicSwapXAMOStrategyProxy delegates calls to a SonicSwapXAMOStrategy implementation */ diff --git a/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol deleted file mode 100644 index 972a4714a5..0000000000 --- a/contracts/contracts/strategies/sonic/SonicCurveAMOStrategy.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import { BaseCurveAMOStrategy } from "../BaseCurveAMOStrategy.sol"; - -/** - * @title Curve AMO Strategy for the Origin Sonic Vault - * @author Origin Protocol Inc - */ -contract SonicCurveAMOStrategy is BaseCurveAMOStrategy { - constructor( - BaseStrategyConfig memory _baseConfig, - address _os, - address _ws, - address _gauge, - address _gaugeFactory, - uint128 _osCoinIndex, - uint128 _wsCoinIndex - ) - BaseCurveAMOStrategy( - _baseConfig, - _os, - _ws, - _gauge, - _gaugeFactory, - _osCoinIndex, - _wsCoinIndex - ) - {} -} diff --git a/contracts/deploy/sonic/014_curve_amo.js b/contracts/deploy/sonic/014_curve_amo.js deleted file mode 100644 index 908665cca2..0000000000 --- a/contracts/deploy/sonic/014_curve_amo.js +++ /dev/null @@ -1,102 +0,0 @@ -const { deployOnSonic } = require("../../utils/deploy-l2"); -const { - deployWithConfirmation, - withConfirmation, -} = require("../../utils/deploy"); -const addresses = require("../../utils/addresses"); -const { oethUnits } = require("../../test/helpers"); - -module.exports = deployOnSonic( - { - deployName: "014_curve_amo", - }, - async ({ ethers }) => { - const { deployerAddr } = await getNamedAccounts(); - const sDeployer = await ethers.provider.getSigner(deployerAddr); - - // Get exiting contracts - const cOSonicProxy = await ethers.getContract("OSonicProxy"); - const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); - const cOSonicVaultAdmin = await ethers.getContractAt( - "OSonicVaultAdmin", - cOSonicVaultProxy.address - ); - const cHarvesterProxy = await ethers.getContract("OSonicHarvesterProxy"); - const cHarvester = await ethers.getContractAt( - "OETHHarvesterSimple", - cHarvesterProxy.address - ); - - // Deploy Sonic Curve AMO Strategy proxy - const dSonicCurveAMOStrategyProxy = await deployWithConfirmation( - "SonicCurveAMOStrategyProxy", - [] - ); - - const cSonicCurveAMOStrategyProxy = await ethers.getContract( - "SonicCurveAMOStrategyProxy" - ); - - // Deploy Sonic Curve AMO Strategy implementation - const dSonicCurveAMOStrategy = await deployWithConfirmation( - "SonicCurveAMOStrategy", - [ - [addresses.sonic.WS_OS.pool, cOSonicVaultProxy.address], - cOSonicProxy.address, - addresses.sonic.wS, - addresses.sonic.WS_OS.gauge, - addresses.sonic.childLiquidityGaugeFactory, - 0, // The OToken (OS for Sonic) is coin 0 of the Curve OS/wS pool - 1, // The WETH token (wS for Sonic) is coin 1 of the Curve OS/wS pool - ] - ); - const cSonicCurveAMOStrategy = await ethers.getContractAt( - "SonicCurveAMOStrategy", - dSonicCurveAMOStrategyProxy.address - ); - - // Initialize Sonic Curve AMO Strategy implementation - const initData = cSonicCurveAMOStrategy.interface.encodeFunctionData( - "initialize(address[],uint256)", - [[addresses.sonic.CRV], oethUnits("0.002")] - ); - await withConfirmation( - // prettier-ignore - cSonicCurveAMOStrategyProxy - .connect(sDeployer)["initialize(address,address,bytes)"]( - dSonicCurveAMOStrategy.address, - addresses.sonic.timelock, - initData - ) - ); - - return { - actions: [ - // 3. Approve strategy on vault - { - contract: cOSonicVaultAdmin, - signature: "approveStrategy(address)", - args: [cSonicCurveAMOStrategyProxy.address], - }, - // 4. Add strategy to mint whitelist - { - contract: cOSonicVaultAdmin, - signature: "addStrategyToMintWhitelist(address)", - args: [cSonicCurveAMOStrategyProxy.address], - }, - // 5. Enable for Curve AMO after it has been deployed - { - contract: cHarvester, - signature: "setSupportedStrategy(address,bool)", - args: [cSonicCurveAMOStrategyProxy.address, true], - }, - // 6. Set the Harvester on the Curve AMO strategy - { - contract: cSonicCurveAMOStrategy, - signature: "setHarvesterAddress(address)", - args: [cHarvesterProxy.address], - }, - ], - }; - } -); diff --git a/contracts/docs/SonicCurveAMOStrategyHierarchy.svg b/contracts/docs/SonicCurveAMOStrategyHierarchy.svg deleted file mode 100644 index d2c3db1806..0000000000 --- a/contracts/docs/SonicCurveAMOStrategyHierarchy.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - -UmlClassDiagram - - - -21 - -Governable -../contracts/governance/Governable.sol - - - -229 - -BaseCurveAMOStrategy -../contracts/strategies/BaseCurveAMOStrategy.sol - - - -290 - -<<Abstract>> -InitializableAbstractStrategy -../contracts/utils/InitializableAbstractStrategy.sol - - - -229->290 - - - - - -410 - -SonicCurveAMOStrategy -../contracts/strategies/sonic/SonicCurveAMOStrategy.sol - - - -410->229 - - - - - -289 - -<<Abstract>> -Initializable -../contracts/utils/Initializable.sol - - - -290->21 - - - - - -290->289 - - - - - diff --git a/contracts/docs/SonicCurveAMOStrategySquashed.svg b/contracts/docs/SonicCurveAMOStrategySquashed.svg deleted file mode 100644 index bf0f450ae2..0000000000 --- a/contracts/docs/SonicCurveAMOStrategySquashed.svg +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - -UmlClassDiagram - - - -410 - -SonicCurveAMOStrategy -../contracts/strategies/sonic/SonicCurveAMOStrategy.sol - -Private: -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   ______gap: uint256[50] <<Initializable>> -   governorPosition: bytes32 <<Governable>> -   pendingGovernorPosition: bytes32 <<Governable>> -   reentryStatusPosition: bytes32 <<Governable>> -   _deprecated_platformAddress: address <<InitializableAbstractStrategy>> -   _deprecated_vaultAddress: address <<InitializableAbstractStrategy>> -   _deprecated_rewardTokenAddress: address <<InitializableAbstractStrategy>> -   _deprecated_rewardLiquidationThreshold: uint256 <<InitializableAbstractStrategy>> -   _reserved: int256[98] <<InitializableAbstractStrategy>> -Internal: -   assetsMapped: address[] <<InitializableAbstractStrategy>> -Public: -   _NOT_ENTERED: uint256 <<Governable>> -   _ENTERED: uint256 <<Governable>> -   platformAddress: address <<InitializableAbstractStrategy>> -   vaultAddress: address <<InitializableAbstractStrategy>> -   assetToPToken: mapping(address=>address) <<InitializableAbstractStrategy>> -   harvesterAddress: address <<InitializableAbstractStrategy>> -   rewardTokenAddresses: address[] <<InitializableAbstractStrategy>> -   SOLVENCY_THRESHOLD: uint256 <<BaseCurveAMOStrategy>> -   weth: IWETH9 <<BaseCurveAMOStrategy>> -   oeth: IERC20 <<BaseCurveAMOStrategy>> -   lpToken: IERC20 <<BaseCurveAMOStrategy>> -   curvePool: ICurveStableSwapNG <<BaseCurveAMOStrategy>> -   gauge: ICurveXChainLiquidityGauge <<BaseCurveAMOStrategy>> -   gaugeFactory: IChildLiquidityGaugeFactory <<BaseCurveAMOStrategy>> -   oethCoinIndex: uint128 <<BaseCurveAMOStrategy>> -   wethCoinIndex: uint128 <<BaseCurveAMOStrategy>> -   maxSlippage: uint256 <<BaseCurveAMOStrategy>> - -Internal: -    _governor(): (governorOut: address) <<Governable>> -    _pendingGovernor(): (pendingGovernor: address) <<Governable>> -    _setGovernor(newGovernor: address) <<Governable>> -    _setPendingGovernor(newGovernor: address) <<Governable>> -    _changeGovernor(_newGovernor: address) <<Governable>> -    _initialize(_rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<InitializableAbstractStrategy>> -    _collectRewardTokens() <<InitializableAbstractStrategy>> -    _setPTokenAddress(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> -    _abstractSetPToken(_asset: address, _pToken: address) <<BaseCurveAMOStrategy>> -    _deposit(_weth: address, _wethAmount: uint256) <<BaseCurveAMOStrategy>> -    calcTokenToBurn(_wethAmount: uint256): (lpToBurn: uint256) <<BaseCurveAMOStrategy>> -    _withdrawAndRemoveFromPool(_lpTokens: uint256, coinIndex: uint128): (coinsRemoved: uint256) <<BaseCurveAMOStrategy>> -    _solvencyAssert() <<BaseCurveAMOStrategy>> -    _lpWithdraw(_lpAmount: uint256) <<BaseCurveAMOStrategy>> -    _setMaxSlippage(_maxSlippage: uint256) <<BaseCurveAMOStrategy>> -    _approveBase() <<BaseCurveAMOStrategy>> -    _max(a: int256, b: int256): int256 <<BaseCurveAMOStrategy>> -External: -    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> -    claimGovernance() <<Governable>> -    collectRewardTokens() <<onlyHarvester, nonReentrant>> <<BaseCurveAMOStrategy>> -    setRewardTokenAddresses(_rewardTokenAddresses: address[]) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    getRewardTokenAddresses(): address[] <<InitializableAbstractStrategy>> -    setPTokenAddress(_asset: address, _pToken: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    removePToken(_assetIndex: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    setHarvesterAddress(_harvesterAddress: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    safeApproveAllTokens() <<onlyGovernor, nonReentrant>> <<BaseCurveAMOStrategy>> -    deposit(_weth: address, _amount: uint256) <<onlyVault, nonReentrant>> <<BaseCurveAMOStrategy>> -    depositAll() <<onlyVault, nonReentrant>> <<BaseCurveAMOStrategy>> -    withdraw(_recipient: address, _weth: address, _amount: uint256) <<onlyVault, nonReentrant>> <<BaseCurveAMOStrategy>> -    withdrawAll() <<onlyVaultOrGovernor, nonReentrant>> <<BaseCurveAMOStrategy>> -    checkBalance(_asset: address): (balance: uint256) <<BaseCurveAMOStrategy>> -    initialize(_rewardTokenAddresses: address[], _maxSlippage: uint256) <<onlyGovernor, initializer>> <<BaseCurveAMOStrategy>> -    mintAndAddOTokens(_oTokens: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance>> <<BaseCurveAMOStrategy>> -    removeAndBurnOTokens(_lpTokens: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance>> <<BaseCurveAMOStrategy>> -    removeOnlyAssets(_lpTokens: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance>> <<BaseCurveAMOStrategy>> -    setMaxSlippage(_maxSlippage: uint256) <<onlyGovernor>> <<BaseCurveAMOStrategy>> -Public: -    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> PTokenAdded(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> -    <<event>> PTokenRemoved(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> -    <<event>> Deposit(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> -    <<event>> Withdrawal(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> -    <<event>> RewardTokenCollected(recipient: address, rewardToken: address, amount: uint256) <<InitializableAbstractStrategy>> -    <<event>> RewardTokenAddressesUpdated(_oldAddresses: address[], _newAddresses: address[]) <<InitializableAbstractStrategy>> -    <<event>> HarvesterAddressesUpdated(_oldHarvesterAddress: address, _newHarvesterAddress: address) <<InitializableAbstractStrategy>> -    <<event>> MaxSlippageUpdated(newMaxSlippage: uint256) <<BaseCurveAMOStrategy>> -    <<modifier>> initializer() <<Initializable>> -    <<modifier>> onlyGovernor() <<Governable>> -    <<modifier>> nonReentrant() <<Governable>> -    <<modifier>> onlyVault() <<InitializableAbstractStrategy>> -    <<modifier>> onlyHarvester() <<InitializableAbstractStrategy>> -    <<modifier>> onlyVaultOrGovernor() <<InitializableAbstractStrategy>> -    <<modifier>> onlyVaultOrGovernorOrStrategist() <<InitializableAbstractStrategy>> -    <<modifier>> onlyStrategist() <<BaseCurveAMOStrategy>> -    <<modifier>> improvePoolBalance() <<BaseCurveAMOStrategy>> -    constructor() <<Governable>> -    governor(): address <<Governable>> -    isGovernor(): bool <<Governable>> -    constructor(_config: BaseStrategyConfig) <<InitializableAbstractStrategy>> -    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    supportsAsset(_asset: address): bool <<BaseCurveAMOStrategy>> -    constructor(_baseConfig: BaseStrategyConfig, _os: address, _ws: address, _gauge: address, _gaugeFactory: address, _osCoinIndex: uint128, _wsCoinIndex: uint128) <<SonicCurveAMOStrategy>> - - - diff --git a/contracts/docs/SonicCurveAMOStrategyStorage.svg b/contracts/docs/SonicCurveAMOStrategyStorage.svg deleted file mode 100644 index ec832121c3..0000000000 --- a/contracts/docs/SonicCurveAMOStrategyStorage.svg +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - -StorageDiagram - - - -3 - -SonicCurveAMOStrategy <<Contract>> - -slot - -0 - -1-50 - -51 - -52 - -53 - -54 - -55 - -56 - -57 - -58 - -59-156 - -157 - -type: <inherited contract>.variable (bytes) - -unallocated (30) - -bool: Initializable.initializing (1) - -bool: Initializable.initialized (1) - -uint256[50]: Initializable.______gap (1600) - -unallocated (12) - -address: InitializableAbstractStrategy._deprecated_platformAddress (20) - -unallocated (12) - -address: InitializableAbstractStrategy._deprecated_vaultAddress (20) - -mapping(address=>address): InitializableAbstractStrategy.assetToPToken (32) - -address[]: InitializableAbstractStrategy.assetsMapped (32) - -unallocated (12) - -address: InitializableAbstractStrategy._deprecated_rewardTokenAddress (20) - -uint256: InitializableAbstractStrategy._deprecated_rewardLiquidationThreshold (32) - -unallocated (12) - -address: InitializableAbstractStrategy.harvesterAddress (20) - -address[]: InitializableAbstractStrategy.rewardTokenAddresses (32) - -int256[98]: InitializableAbstractStrategy._reserved (3136) - -uint256: BaseCurveAMOStrategy.maxSlippage (32) - - - -1 - -address[]: assetsMapped <<Array>> -0x4a11f94e20a93c79f6ec743a1954ec4fc2c08429ae2122118bf234b2185c81b8 - -offset - -0 - -type: variable (bytes) - -unallocated (12) - -address (20) - - - -3:8->1 - - - - - -2 - -address[]: rewardTokenAddresses <<Array>> -0xa2999d817b6757290b50e8ecf3fa939673403dd35c97de392fdb343b4015ce9e - -offset - -0 - -type: variable (bytes) - -unallocated (12) - -address (20) - - - -3:13->2 - - - - - diff --git a/contracts/docs/generate.sh b/contracts/docs/generate.sh index e2d56117ac..81c8f2b5ca 100644 --- a/contracts/docs/generate.sh +++ b/contracts/docs/generate.sh @@ -118,10 +118,6 @@ sol2uml .. -v -hv -hf -he -hs -hl -hi -b SonicStakingStrategy -o SonicStakingStr sol2uml .. -s -d 0 -b SonicStakingStrategy -o SonicStakingStrategySquashed.svg sol2uml storage .. -c SonicStakingStrategy -o SonicStakingStrategyStorage.svg --hideExpand __gap,______gap,_reserved -sol2uml .. -v -hv -hf -he -hs -hl -hi -b SonicCurveAMOStrategy -o SonicCurveAMOStrategyHierarchy.svg -sol2uml .. -s -d 0 -b SonicCurveAMOStrategy -o SonicCurveAMOStrategySquashed.svg -sol2uml storage .. -c SonicCurveAMOStrategy -o SonicCurveAMOStrategyStorage.svg --hideExpand __gap,______gap,_reserved - sol2uml .. -v -hv -hf -he -hs -hl -hi -b SonicSwapXAMOStrategy -o SonicSwapXAMOStrategyHierarchy.svg sol2uml .. -s -d 0 -b SonicSwapXAMOStrategy -o SonicSwapXAMOStrategySquashed.svg sol2uml storage .. -c SonicSwapXAMOStrategy -o SonicSwapXAMOStrategyStorage.svg --hideExpand __gap,______gap,_reserved diff --git a/contracts/docs/plantuml/sonicContracts.png b/contracts/docs/plantuml/sonicContracts.png index 03924d20e30e919de3d10b6ce84b4c323f873cd4..c10225f9fff4fb9a931761431e712ab5de582392 100644 GIT binary patch literal 53548 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 a<0#V`v+WHt>UbB!S$Wk{uTEUK{r>=|kmCCQ literal 61878 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

  • 5XC7NyTKxtJHLj`tU< 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 diff --git a/contracts/docs/plantuml/sonicContracts.puml b/contracts/docs/plantuml/sonicContracts.puml index 192b3523d4..f7962f3302 100644 --- a/contracts/docs/plantuml/sonicContracts.puml +++ b/contracts/docs/plantuml/sonicContracts.puml @@ -44,7 +44,7 @@ object "OSonicVault" as vault <><> #$originColor { } object "OSonicHarvester" as harv <><> #$originColor { - rewards: CRV, SWPx + rewards: SWPx } ' Oracle @@ -59,21 +59,6 @@ object "Special Fee Contract" as sfc <><> { asset: S } -object "Curve AMO Strategy" as curveAmoStrat <><> #$originColor { - asset: wS - reward: CRV -} - -object "Curve OS/wS\nPool" as curvePool <> { - assets: OS, wS - lp: OSwS -} - -object "Curve OS/wS\nGauge" as curveGauge <> { - asset: OSwS - lp: OSwS-gauge -} - object "SwapX AMO Strategy" as swapXAmoStrat <><> #$originColor { asset: wS reward: SWPx @@ -100,16 +85,11 @@ os <.> vault vault <.> drip vault <...> stakeStrat stakeStrat ..> sfc -vault <...> curveAmoStrat vault .> router vault <.. harv drip <.. harv -harv <..> curveAmoStrat -curveAmoStrat ..> curvePool -curveAmoStrat ..> curveGauge - harv <..> swapXAmoStrat swapXAmoStrat ..> swapXPool swapXAmoStrat ..> swapXGauge diff --git a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js deleted file mode 100644 index 15d57ea487..0000000000 --- a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js +++ /dev/null @@ -1,765 +0,0 @@ -const { createFixtureLoader } = require("../../_fixture"); -const { defaultSonicFixture } = require("../../_fixture-sonic"); -const { expect } = require("chai"); -const { oethUnits } = require("../../helpers"); -const addresses = require("../../../utils/addresses"); -const { impersonateAndFund } = require("../../../utils/signers"); -const { setERC20TokenBalance } = require("../../_fund"); -const hre = require("hardhat"); -const { advanceTime } = require("../../helpers"); -const { shouldBehaveLikeGovernable } = require("../../behaviour/governable"); -const { shouldBehaveLikeHarvestable } = require("../../behaviour/harvestable"); -const { shouldBehaveLikeStrategy } = require("../../behaviour/strategy"); - -const sonicFixture = createFixtureLoader(defaultSonicFixture); - -describe("Sonic Fork Test: Curve AMO strategy", function () { - let fixture, vault, curveAMOStrategy, os, ws, nick, clement, rafael, timelock; - - let curvePool, - curveGauge, - impersonatedVaultSigner, - impersonatedStrategist, - impersonatedHarvester, - impersonatedCurveGaugeFactory, - impersonatedAMOGovernor, - impersonatedCurveStrategy, - curveChildLiquidityGaugeFactory, - impersonatedTimelock, - crv, - harvester; - - let defaultDepositor; - - const defaultDeposit = oethUnits("5"); - - beforeEach(async () => { - fixture = await sonicFixture(); - vault = fixture.oSonicVault; - curveAMOStrategy = fixture.curveAMOStrategy; - os = fixture.oSonic; - ws = fixture.wS; - nick = fixture.nick; - rafael = fixture.rafael; - clement = fixture.clement; - timelock = fixture.timelock; - curvePool = fixture.curvePool; - curveGauge = fixture.curveGauge; - curveChildLiquidityGaugeFactory = fixture.curveChildLiquidityGaugeFactory; - crv = fixture.crv; - harvester = fixture.harvester; - - defaultDepositor = rafael; - - impersonatedVaultSigner = await impersonateAndFund(vault.address); - impersonatedStrategist = await impersonateAndFund( - await vault.strategistAddr() - ); - impersonatedHarvester = await impersonateAndFund(harvester.address); - impersonatedCurveGaugeFactory = await impersonateAndFund( - curveChildLiquidityGaugeFactory.address - ); - impersonatedAMOGovernor = await impersonateAndFund( - await curveAMOStrategy.governor() - ); - impersonatedTimelock = await impersonateAndFund(timelock.address); - impersonatedCurveStrategy = await impersonateAndFund( - curveAMOStrategy.address - ); - - // Set vaultBuffer to 100% - await vault.connect(impersonatedTimelock).setVaultBuffer(oethUnits("1")); - }); - - it("Should have correct parameters after deployment", async () => { - const { curveAMOStrategy } = fixture; - expect(await curveAMOStrategy.platformAddress()).to.equal( - addresses.sonic.WS_OS.pool - ); - expect(await curveAMOStrategy.vaultAddress()).to.equal(vault.address); - expect(await curveAMOStrategy.gauge()).to.equal( - addresses.sonic.WS_OS.gauge - ); - expect(await curveAMOStrategy.curvePool()).to.equal( - addresses.sonic.WS_OS.pool - ); - expect(await curveAMOStrategy.lpToken()).to.equal( - addresses.sonic.WS_OS.pool - ); - expect(await curveAMOStrategy.oeth()).to.equal(os.address); - expect(await curveAMOStrategy.weth()).to.equal(ws.address); - expect(await curveAMOStrategy.governor()).to.equal( - addresses.sonic.timelock - ); - expect(await curveAMOStrategy.rewardTokenAddresses(0)).to.equal( - addresses.sonic.CRV - ); - expect(await curveAMOStrategy.maxSlippage()).to.equal(oethUnits("0.002")); - }); - - describe("Operational functions", () => { - it("Should deposit to strategy", async () => { - await balancePool(); - - const checkBalanceBefore = await curveAMOStrategy.checkBalance( - ws.address - ); - const gaugeBalanceBefore = await curveGauge.balanceOf( - curveAMOStrategy.address - ); - await mintAndDepositToStrategy(); - - expect( - (await curveAMOStrategy.checkBalance(ws.address)).sub( - checkBalanceBefore - ) - ).to.approxEqual(defaultDeposit.mul(2)); - expect( - (await curveGauge.balanceOf(curveAMOStrategy.address)).sub( - gaugeBalanceBefore - ) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); - expect(await os.balanceOf(defaultDepositor.address)).to.equal( - defaultDeposit - ); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); - }); - - it("Should deposit all to strategy", async () => { - await balancePool(); - - const amount = defaultDeposit; - const user = defaultDepositor; - const checkBalanceBefore = await curveAMOStrategy.checkBalance( - ws.address - ); - const gaugeBalanceBefore = await curveGauge.balanceOf( - curveAMOStrategy.address - ); - - const balance = await ws.balanceOf(user.address); - if (balance < amount) { - await setERC20TokenBalance(user.address, ws, amount + balance, hre); - } - await ws.connect(user).transfer(curveAMOStrategy.address, amount); - - expect(await ws.balanceOf(curveAMOStrategy.address)).to.gt(0); - await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); - - expect( - (await curveAMOStrategy.checkBalance(ws.address)).sub( - checkBalanceBefore - ) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); - expect( - (await curveGauge.balanceOf(curveAMOStrategy.address)).sub( - gaugeBalanceBefore - ) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); - }); - - it("Should deposit all to strategy with no balance", async () => { - await balancePool(); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); - - await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); - - expect(await curveAMOStrategy.checkBalance(ws.address)).to.eq(0); - expect(await curveGauge.balanceOf(curveAMOStrategy.address)).to.eq(0); - }); - - it("Should withdraw from strategy", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - - const impersonatedVaultSigner = await impersonateAndFund(vault.address); - - const checkBalanceBefore = await curveAMOStrategy.checkBalance( - ws.address - ); - const gaugeBalanceBefore = await curveGauge.balanceOf( - curveAMOStrategy.address - ); - - await curveAMOStrategy - .connect(impersonatedVaultSigner) - .withdraw(vault.address, ws.address, oethUnits("1")); - - expect( - checkBalanceBefore.sub(await curveAMOStrategy.checkBalance(ws.address)) - ).to.approxEqualTolerance(oethUnits("1").mul(2)); - expect( - gaugeBalanceBefore.sub( - await curveGauge.balanceOf(curveAMOStrategy.address) - ) - ).to.approxEqualTolerance(oethUnits("1").mul(2)); - expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( - oethUnits("0") - ); - }); - - it("Should withdraw all from strategy", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - - const balanceVault = await ws.balanceOf(vault.address); - - await curveAMOStrategy.connect(impersonatedVaultSigner).withdrawAll(); - - expect( - await curveAMOStrategy.checkBalance(ws.address) - ).to.approxEqualTolerance(0); - expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(0); - expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( - oethUnits("0") - ); - expect(await ws.balanceOf(vault.address)).to.approxEqualTolerance( - balanceVault.add(defaultDeposit) - ); - }); - - it("Should mintAndAddOToken", async () => { - await unbalancePool({ - balancedBefore: true, - wsAmount: defaultDeposit, - }); - - await curveAMOStrategy - .connect(impersonatedStrategist) - .mintAndAddOTokens(defaultDeposit); - - expect( - await curveAMOStrategy.checkBalance(ws.address) - ).to.approxEqualTolerance(defaultDeposit); - expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(defaultDeposit); - expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( - oethUnits("0") - ); - }); - - it("Should removeAndBurnOToken", async () => { - await balancePool(); - await mintAndDepositToStrategy({ - userOverride: false, - amount: defaultDeposit.mul(2), - returnTransaction: false, - }); - await unbalancePool({ - balancedBefore: true, - osAmount: defaultDeposit.mul(2), - }); - - await curveAMOStrategy - .connect(impersonatedStrategist) - .removeAndBurnOTokens(defaultDeposit); - - expect( - await curveAMOStrategy.checkBalance(ws.address) - ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); - expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); - expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( - oethUnits("0") - ); - }); - - it("Should removeOnlyAssets", async () => { - await balancePool(); - await mintAndDepositToStrategy({ - userOverride: false, - amount: defaultDeposit.mul(2), - returnTransaction: false, - }); - await unbalancePool({ - balancedBefore: true, - wsAmount: defaultDeposit.mul(2), - }); - - const vaultETHBalanceBefore = await ws.balanceOf(vault.address); - - await curveAMOStrategy - .connect(impersonatedStrategist) - .removeOnlyAssets(defaultDeposit); - - expect( - await curveAMOStrategy.checkBalance(ws.address) - ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); - expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); - expect(await ws.balanceOf(vault.address)).to.approxEqualTolerance( - vaultETHBalanceBefore.add(defaultDeposit) - ); - }); - - it("Should collectRewardTokens", async () => { - await mintAndDepositToStrategy(); - await simulateCRVInflation({ - amount: oethUnits("1000000"), - timejump: 60, - checkpoint: true, - }); - - const balanceCRVHarvesterBefore = await crv.balanceOf(harvester.address); - await curveAMOStrategy - .connect(impersonatedHarvester) - .collectRewardTokens(); - const balanceCRVHarvesterAfter = await crv.balanceOf(harvester.address); - - expect(balanceCRVHarvesterAfter).to.be.gt(balanceCRVHarvesterBefore); - expect(await crv.balanceOf(curveGauge.address)).to.equal(0); - }); - }); - - describe("when pool is heavily unbalanced", () => { - it("Should deposit with OS", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - - await unbalancePool({ osAmount: defaultDeposit.mul(1000) }); - - await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); - - expect( - await curveAMOStrategy.checkBalance(ws.address) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); - expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); - }); - - it("Should deposit with wS", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - - await unbalancePool({ wsAmount: defaultDeposit.mul(1000) }); - - await curveAMOStrategy.connect(impersonatedVaultSigner).depositAll(); - - expect( - await curveAMOStrategy.checkBalance(ws.address) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); - expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(defaultDeposit.mul(2)); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal(0); - }); - - it("Should withdraw all with OS", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - - await unbalancePool({ osAmount: defaultDeposit.mul(1000) }); - - const checkBalanceAMO = await curveAMOStrategy.checkBalance(ws.address); - const balanceVault = await ws.balanceOf(vault.address); - - await curveAMOStrategy.connect(impersonatedVaultSigner).withdrawAll(); - - expect( - await curveAMOStrategy.checkBalance(ws.address) - ).to.approxEqualTolerance(0); - expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(0); - expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( - oethUnits("0") - ); - expect(await ws.balanceOf(vault.address)).to.approxEqualTolerance( - balanceVault.add(checkBalanceAMO) - ); - }); - - it("Should withdraw all with wS", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - - await unbalancePool({ wsAmount: defaultDeposit.mul(1000) }); - const checkBalanceAMO = await curveAMOStrategy.checkBalance(ws.address); - const balanceVault = await ws.balanceOf(vault.address); - - await curveAMOStrategy.connect(impersonatedVaultSigner).withdrawAll(); - - expect( - await curveAMOStrategy.checkBalance(ws.address) - ).to.approxEqualTolerance(0); - expect( - await curveGauge.balanceOf(curveAMOStrategy.address) - ).to.approxEqualTolerance(0); - expect(await os.balanceOf(curveAMOStrategy.address)).to.equal(0); - expect(await ws.balanceOf(curveAMOStrategy.address)).to.equal( - oethUnits("0") - ); - expect(await ws.balanceOf(vault.address)).to.approxEqualTolerance( - balanceVault.add(checkBalanceAMO) - ); - }); - }); - - describe("admin functions", () => { - it("Should set max slippage", async () => { - await curveAMOStrategy - .connect(impersonatedAMOGovernor) - .setMaxSlippage(oethUnits("0.01456")); - - expect(await curveAMOStrategy.maxSlippage()).to.equal( - oethUnits("0.01456") - ); - }); - }); - - describe("Should revert when", () => { - it("Deposit: Must deposit something", async () => { - await expect( - curveAMOStrategy.connect(impersonatedVaultSigner).deposit(ws.address, 0) - ).to.be.revertedWith("Must deposit something"); - }); - it("Deposit: Can only deposit wS", async () => { - await expect( - curveAMOStrategy - .connect(impersonatedVaultSigner) - .deposit(os.address, defaultDeposit) - ).to.be.revertedWith("Can only deposit WETH"); - }); - it("Deposit: Caller is not the Vault", async () => { - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .deposit(ws.address, defaultDeposit) - ).to.be.revertedWith("Caller is not the Vault"); - }); - it("Deposit: Protocol is insolvent", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - - // Make protocol insolvent by minting a lot of OETH - // This is a cheat. - // prettier-ignore - await vault - .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); - - await expect( - mintAndDepositToStrategy({ returnTransaction: true }) - ).to.be.revertedWith("Protocol insolvent"); - }); - it("Withdraw: Must withdraw something", async () => { - await expect( - curveAMOStrategy - .connect(impersonatedVaultSigner) - .withdraw(vault.address, ws.address, 0) - ).to.be.revertedWith("Must withdraw something"); - }); - it("Withdraw: Can only withdraw WETH", async () => { - await expect( - curveAMOStrategy - .connect(impersonatedVaultSigner) - .withdraw(vault.address, os.address, defaultDeposit) - ).to.be.revertedWith("Can only withdraw WETH"); - }); - it("Withdraw: Caller is not the vault", async () => { - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .withdraw(vault.address, ws.address, defaultDeposit) - ).to.be.revertedWith("Caller is not the Vault"); - }); - it("Withdraw: Amount is greater than balance", async () => { - await expect( - curveAMOStrategy - .connect(impersonatedVaultSigner) - .withdraw(vault.address, ws.address, oethUnits("1000000")) - ).to.be.revertedWith(""); - }); - it("Withdraw: Protocol is insolvent", async () => { - await balancePool(); - await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); - - // Make protocol insolvent by minting a lot of OETH and send them - // Otherwise they will be burned and the protocol will not be insolvent. - // This is a cheat. - // prettier-ignore - await vault - .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); - await os - .connect(impersonatedCurveStrategy) - .transfer(vault.address, oethUnits("1000000")); - - await expect( - curveAMOStrategy - .connect(impersonatedVaultSigner) - .withdraw(vault.address, ws.address, defaultDeposit) - ).to.be.revertedWith("Protocol insolvent"); - }); - it("Mint OToken: Asset overshot peg", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - await unbalancePool({ wsAmount: defaultDeposit }); // +5 WETH in the pool - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .mintAndAddOTokens(defaultDeposit.mul(2)) - ).to.be.revertedWith("Assets overshot peg"); - }); - it("Mint OToken: OTokens balance worse", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - await unbalancePool({ osAmount: defaultDeposit.mul(2) }); // +10 OETH in the pool - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .mintAndAddOTokens(defaultDeposit) - ).to.be.revertedWith("OTokens balance worse"); - }); - it("Mint OToken: Protocol insolvent", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - // prettier-ignore - await vault - .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .mintAndAddOTokens(defaultDeposit) - ).to.be.revertedWith("Protocol insolvent"); - }); - it("Burn OToken: Asset balance worse", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - await unbalancePool({ wsAmount: defaultDeposit.mul(2) }); // +10 WETH in the pool - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .removeAndBurnOTokens(defaultDeposit) - ).to.be.revertedWith("Assets balance worse"); - }); - it("Burn OToken: OTokens overshot peg", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - await unbalancePool({ osAmount: defaultDeposit }); // +5 OETH in the pool - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .removeAndBurnOTokens(defaultDeposit) - ).to.be.revertedWith("OTokens overshot peg"); - }); - it("Burn OToken: Protocol insolvent", async () => { - await balancePool(); - await mintAndDepositToStrategy(); - // prettier-ignore - await vault - .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .removeAndBurnOTokens(defaultDeposit) - ).to.be.revertedWith("Protocol insolvent"); - }); - it("Remove only assets: Asset overshot peg", async () => { - await balancePool(); - await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); - await unbalancePool({ wsAmount: defaultDeposit.mul(2) }); // +10 WETH in the pool - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .removeOnlyAssets(defaultDeposit.mul(3)) - ).to.be.revertedWith("Assets overshot peg"); - }); - it("Remove only assets: OTokens balance worse", async () => { - await balancePool(); - await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); - await unbalancePool({ osAmount: defaultDeposit.mul(2) }); // +10 OETH in the pool - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .removeOnlyAssets(defaultDeposit) - ).to.be.revertedWith("OTokens balance worse"); - }); - it("Remove only assets: Protocol insolvent", async () => { - await balancePool(); - await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); - // prettier-ignore - await vault - .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); - await expect( - curveAMOStrategy - .connect(impersonatedStrategist) - .removeOnlyAssets(defaultDeposit) - ).to.be.revertedWith("Protocol insolvent"); - }); - it("Check balance: Unsupported asset", async () => { - await expect( - curveAMOStrategy.checkBalance(os.address) - ).to.be.revertedWith("Unsupported asset"); - }); - it("Max slippage is too high", async () => { - await expect( - curveAMOStrategy - .connect(impersonatedAMOGovernor) - .setMaxSlippage(oethUnits("0.51")) - ).to.be.revertedWith("Slippage must be less than 100%"); - }); - }); - - shouldBehaveLikeGovernable(() => ({ - ...fixture, - anna: rafael, - josh: nick, - matt: clement, - dai: crv, - strategy: curveAMOStrategy, - governor: timelock, - })); - - shouldBehaveLikeHarvestable(() => ({ - ...fixture, - anna: rafael, - strategy: curveAMOStrategy, - harvester, - oeth: os, - governor: timelock, - })); - - shouldBehaveLikeStrategy(() => ({ - ...fixture, - // Contracts - strategy: curveAMOStrategy, - checkWithdrawAmounts: false, - vault: vault, - assets: [ws], - governor: timelock, - strategist: rafael, - harvester, - crv, - // As we don't have this on base fixture, we use CRV - usdt: crv, - usdc: crv, - dai: crv, - weth: ws, - reth: crv, - stETH: crv, - frxETH: crv, - cvx: crv, - comp: crv, - bal: crv, - // Users - anna: rafael, - matt: clement, - josh: nick, - })); - - const mintAndDepositToStrategy = async ({ - userOverride, - amount, - returnTransaction, - } = {}) => { - const user = userOverride || defaultDepositor; - amount = amount || defaultDeposit; - - const balance = await ws.balanceOf(user.address); - if (balance < amount) { - await setERC20TokenBalance(user.address, ws, amount + balance, hre); - } - await ws.connect(user).approve(vault.address, amount); - await vault.connect(user).mint(ws.address, amount, amount); - - const gov = await vault.governor(); - const tx = await vault - .connect(await impersonateAndFund(gov)) - .depositToStrategy(curveAMOStrategy.address, [ws.address], [amount]); - - if (returnTransaction) { - return tx; - } - - await expect(tx).to.emit(curveAMOStrategy, "Deposit"); - }; - - const balancePool = async () => { - let balances = await curvePool.get_balances(); - const balanceOS = balances[0]; - const balanceWS = balances[1]; - - if (balanceWS > balanceOS) { - const amount = balanceWS.sub(balanceOS); - const balance = ws.balanceOf(nick.address); - if (balance < amount) { - await setERC20TokenBalance(nick.address, ws, amount + balance, hre); - } - await ws.connect(nick).approve(vault.address, amount.mul(101).div(10)); - await vault - .connect(nick) - .mint(ws.address, amount.mul(101).div(10), amount); - await os.connect(nick).approve(curvePool.address, amount); - // prettier-ignore - await curvePool - .connect(nick)["add_liquidity(uint256[],uint256)"]([amount, 0], 0); - } else if (balanceWS < balanceOS) { - const amount = balanceOS.sub(balanceWS); - const balance = ws.balanceOf(nick.address); - if (balance < amount) { - await setERC20TokenBalance(nick.address, ws, amount + balance, hre); - } - await ws.connect(nick).approve(curvePool.address, amount); - // prettier-ignore - await curvePool - .connect(nick)["add_liquidity(uint256[],uint256)"]([0, amount], 0); - } - - balances = await curvePool.get_balances(); - expect(balances[0]).to.approxEqualTolerance(balances[1]); - }; - - const unbalancePool = async ({ balancedBefore, wsAmount, osAmount } = {}) => { - if (balancedBefore) { - await balancePool(); - } - - if (wsAmount) { - const balance = ws.balanceOf(nick.address); - if (balance < wsAmount) { - await setERC20TokenBalance(nick.address, ws, wsAmount + balance, hre); - } - await ws.connect(nick).approve(curvePool.address, wsAmount); - // prettier-ignore - await curvePool - .connect(nick)["add_liquidity(uint256[],uint256)"]([0, wsAmount], 0); - } else { - const balance = ws.balanceOf(nick.address); - if (balance < osAmount) { - await setERC20TokenBalance(nick.address, ws, osAmount + balance, hre); - } - await ws.connect(nick).approve(vault.address, osAmount); - await vault.connect(nick).mint(ws.address, osAmount, osAmount); - await os.connect(nick).approve(curvePool.address, osAmount); - // prettier-ignore - await curvePool - .connect(nick)["add_liquidity(uint256[],uint256)"]([osAmount, 0], 0); - } - }; - - const simulateCRVInflation = async ({ - amount, - timejump, - checkpoint, - } = {}) => { - await setERC20TokenBalance(curveGauge.address, crv, amount, hre); - await advanceTime(timejump); - if (checkpoint) { - curveGauge - .connect(impersonatedCurveGaugeFactory) - .user_checkpoint(curveAMOStrategy.address); - } - }; -}); From 9af2bc36b092c8d61cf861d7c7d388735de4712c Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 11 Mar 2025 14:48:08 +1100 Subject: [PATCH 78/80] Removed Curve AMO from Sonic fixture --- contracts/test/_fixture-sonic.js | 45 +------------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 316bbe1afb..85a75ba829 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -10,11 +10,6 @@ const { impersonateAndFund } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); const addresses = require("../utils/addresses"); -const erc20Abi = require("./abi/erc20.json"); -const curveXChainLiquidityGaugeAbi = require("./abi/curveXChainLiquidityGauge.json"); -const curveStableSwapNGAbi = require("./abi/curveStableSwapNG.json"); -const curveChildLiquidityGaugeFactoryAbi = require("./abi/curveChildLiquidityGaugeFactory.json"); - const log = require("../utils/logger")("test:fixtures-sonic"); const MINTER_ROLE = @@ -152,12 +147,7 @@ const defaultSonicFixture = deployments.createFixture(async () => { const [minter, burner, rafael, nick, clement] = signers.slice(4); // Skip first 4 addresses to avoid conflict - let validatorRegistrator, - curveAMOStrategy, - curvePool, - curveGauge, - curveChildLiquidityGaugeFactory, - crv; + let validatorRegistrator; if (isFork) { validatorRegistrator = await impersonateAndFund( addresses.sonic.validatorRegistrator @@ -165,32 +155,6 @@ const defaultSonicFixture = deployments.createFixture(async () => { validatorRegistrator.address = addresses.sonic.validatorRegistrator; await sonicStakingStrategy.connect(strategist).setDefaultValidatorId(18); - - // Curve AMO - const curveAMOProxy = await ethers.getContract( - "SonicCurveAMOStrategyProxy" - ); - curveAMOStrategy = await ethers.getContractAt( - "SonicCurveAMOStrategy", - curveAMOProxy.address - ); - - curvePool = await ethers.getContractAt( - curveStableSwapNGAbi, - addresses.sonic.WS_OS.pool - ); - - curveGauge = await ethers.getContractAt( - curveXChainLiquidityGaugeAbi, - addresses.sonic.WS_OS.gauge - ); - - curveChildLiquidityGaugeFactory = await ethers.getContractAt( - curveChildLiquidityGaugeFactoryAbi, - addresses.sonic.childLiquidityGaugeFactory - ); - - crv = await ethers.getContractAt(erc20Abi, addresses.sonic.CRV); } for (const user of [rafael, nick, clement]) { @@ -218,13 +182,6 @@ const defaultSonicFixture = deployments.createFixture(async () => { // Wrapped S wS, - // Curve - curveAMOStrategy, - curvePool, - curveGauge, - curveChildLiquidityGaugeFactory, - crv, - // Signers governor, strategist, From 031416d1bd74a32de9eb7aba501f73879444fa36 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 11 Mar 2025 15:41:58 +1100 Subject: [PATCH 79/80] Fixed Sonic fork tests --- .../sonic/swapx-amo.sonic.fork-test.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index c73df46d35..a6f4f825ff 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -225,7 +225,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { const tx = swapXAMOStrategy .connect(strategist) - .swapAssetsToPool(parseUnits("5000")); + .swapAssetsToPool(parseUnits("5500")); await expect(tx).to.be.revertedWith("Assets overshot peg"); }); @@ -452,10 +452,10 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); // Assert the strategy's balance - expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( - dataBefore.stratBalance, + expect( + await swapXAMOStrategy.checkBalance(wS.address), "Strategy's check balance" - ); + ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); // Swap wS into the pool and OS out await poolSwapTokensIn(wS, parseUnits("2000000")); @@ -463,10 +463,10 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); // Assert the strategy's balance - expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( - dataBefore.stratBalance, + expect( + await swapXAMOStrategy.checkBalance(wS.address), "Strategy's check balance" - ); + ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); }); it("a lot of wS is swapped into the pool", async () => { const { swapXAMOStrategy, oSonic, wS } = fixture; @@ -477,10 +477,10 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); // Assert the strategy's balance - expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( - dataBefore.stratBalance, + expect( + await swapXAMOStrategy.checkBalance(wS.address), "Strategy's check balance" - ); + ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); // Swap OS into the pool and wS out await poolSwapTokensIn(oSonic, parseUnits("1005000")); @@ -488,10 +488,10 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); // Assert the strategy's balance - expect(await swapXAMOStrategy.checkBalance(wS.address)).to.equal( - dataBefore.stratBalance, + expect( + await swapXAMOStrategy.checkBalance(wS.address), "Strategy's check balance" - ); + ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); }); }); From 171b7c1b06c985606b6a1cae122b84f336a7bde4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 11 Mar 2025 16:10:04 +1100 Subject: [PATCH 80/80] Fixed Curve AMO fork tests --- .../test/strategies/sonic/curve-amo.sonic.fork-test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js index 15d57ea487..3babda3649 100644 --- a/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/curve-amo.sonic.fork-test.js @@ -1,5 +1,5 @@ const { createFixtureLoader } = require("../../_fixture"); -const { defaultSonicFixture } = require("../../_fixture-sonic"); +const { swapXAMOFixture } = require("../../_fixture-sonic"); const { expect } = require("chai"); const { oethUnits } = require("../../helpers"); const addresses = require("../../../utils/addresses"); @@ -10,8 +10,12 @@ const { advanceTime } = require("../../helpers"); const { shouldBehaveLikeGovernable } = require("../../behaviour/governable"); const { shouldBehaveLikeHarvestable } = require("../../behaviour/harvestable"); const { shouldBehaveLikeStrategy } = require("../../behaviour/strategy"); +const { parseUnits } = require("ethers/lib/utils"); -const sonicFixture = createFixtureLoader(defaultSonicFixture); +const sonicFixture = createFixtureLoader(swapXAMOFixture, { + // Mint a little so any shortfall of wS in the Vault can be covered + wsMintAmount: 10, +}); describe("Sonic Fork Test: Curve AMO strategy", function () { let fixture, vault, curveAMOStrategy, os, ws, nick, clement, rafael, timelock; @@ -226,7 +230,7 @@ describe("Sonic Fork Test: Curve AMO strategy", function () { it("Should mintAndAddOToken", async () => { await unbalancePool({ balancedBefore: true, - wsAmount: defaultDeposit, + wsAmount: parseUnits("6", 18), }); await curveAMOStrategy