diff --git a/contracts/contracts/harvest/OETHHarvesterSimple.sol b/contracts/contracts/harvest/OETHHarvesterSimple.sol index 6acc3f0944..96603fecb1 100644 --- a/contracts/contracts/harvest/OETHHarvesterSimple.sol +++ b/contracts/contracts/harvest/OETHHarvesterSimple.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import { Strategizable } from "../governance/Strategizable.sol"; diff --git a/contracts/contracts/harvest/SuperOETHHarvester.sol b/contracts/contracts/harvest/SuperOETHHarvester.sol index e0b368b1cc..5dceef36a6 100644 --- a/contracts/contracts/harvest/SuperOETHHarvester.sol +++ b/contracts/contracts/harvest/SuperOETHHarvester.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import { OETHHarvesterSimple, IERC20, IStrategy, SafeERC20 } from "./OETHHarvesterSimple.sol"; diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 3554cd1521..2bb5b319c2 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -105,7 +105,7 @@ interface IVault { function setOracleSlippage(address _asset, uint16 _allowedOracleSlippageBps) external; - function supportAsset(address _asset, uint8 _supportsAsset) external; + function supportAsset(address _asset, uint8 _unitConversion) external; function approveStrategy(address _addr) external; diff --git a/contracts/contracts/oracle/OracleRouter.sol b/contracts/contracts/oracle/OracleRouter.sol index 46ea56de66..25f04bec6d 100644 --- a/contracts/contracts/oracle/OracleRouter.sol +++ b/contracts/contracts/oracle/OracleRouter.sol @@ -28,6 +28,17 @@ contract OracleRouter is AbstractOracleRouter { // Chainlink: DAI/USD feedAddress = 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9; maxStaleness = 1 hours + STALENESS_BUFFER; + } else if (asset == 0xdC035D45d973E3EC169d2276DDab16f1e407384F) { + // https://data.chain.link/ethereum/mainnet/stablecoins/dai-usd + // Chainlink: DAI/USD + feedAddress = 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9; + maxStaleness = 1 hours + STALENESS_BUFFER; + // } else if (asset == 0xdC035D45d973E3EC169d2276DDab16f1e407384F) { + // solhint-disable-next-line + // // https://chroniclelabs.org/dashboard/oracle/USDS/USD?blockchain=ETH&txn=0x963edc177ee0cb8e5ecdb39b535b800c5037b2e2fc20b335e44a95a979d4719a&contract=0x74661a9ea74fD04975c6eBc6B155Abf8f885636c + // // Chronicle: USDS/USD + // feedAddress = 0x74661a9ea74fD04975c6eBc6B155Abf8f885636c; + // maxStaleness = 1 hours + STALENESS_BUFFER; } else if (asset == 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) { // https://data.chain.link/ethereum/mainnet/stablecoins/usdc-usd // Chainlink: USDC/USD diff --git a/contracts/contracts/proxies/BaseProxies.sol b/contracts/contracts/proxies/BaseProxies.sol index b7e30eba72..1113829869 100644 --- a/contracts/contracts/proxies/BaseProxies.sol +++ b/contracts/contracts/proxies/BaseProxies.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import { InitializeGovernedUpgradeabilityProxy } from "./InitializeGovernedUpgradeabilityProxy.sol"; diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index f3cb4d3c35..f49a349349 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import { InitializeGovernedUpgradeabilityProxy } from "./InitializeGovernedUpgradeabilityProxy.sol"; @@ -332,3 +332,10 @@ contract PoolBoostCentralRegistryProxy is { } + +/** + * @notice MakerSSRStrategyProxy delegates calls to a Generalized4626Strategy implementation + */ +contract MakerSSRStrategyProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/contracts/strategies/DAIMigratorStrategy.sol b/contracts/contracts/strategies/DAIMigratorStrategy.sol new file mode 100644 index 0000000000..46c989b48b --- /dev/null +++ b/contracts/contracts/strategies/DAIMigratorStrategy.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IDaiUsdsMigrationContract { + function daiToUsds(address usr, uint256 wad) external; + + function usdsToDai(address usr, uint256 wad) external; +} + +contract DAIMigrationStrategy is InitializableAbstractStrategy { + address public immutable dai; + address public immutable usds; + + constructor( + BaseStrategyConfig memory _baseConfig, + address _dai, + address _usds + ) InitializableAbstractStrategy(_baseConfig) { + dai = _dai; + usds = _usds; + } + + function initialize(address _governorAddr) + external + onlyGovernor + initializer + { + _setGovernor(_governorAddr); + + address[] memory rewardTokens = new address[](0); + address[] memory assets = new address[](2); + address[] memory pTokens = new address[](2); + + assets[0] = address(dai); + assets[1] = address(usds); + pTokens[0] = address(dai); + pTokens[1] = address(usds); + + InitializableAbstractStrategy._initialize( + rewardTokens, + assets, + pTokens + ); + } + + function deposit(address _asset, uint256 _amount) + external + override + onlyVault + nonReentrant + { + _deposit(_asset, _amount); + } + + function depositAll() external override onlyVault nonReentrant { + _deposit(dai, IERC20(dai).balanceOf(address(this))); + } + + function _deposit(address _asset, uint256 _amount) internal { + // You can only deposit DAI + require(_asset == dai, "Only DAI can be deposited"); + require(_amount > 0, "Must deposit something"); + + IERC20(dai).approve(platformAddress, _amount); + IDaiUsdsMigrationContract(platformAddress).daiToUsds( + address(this), + _amount + ); + } + + function withdraw( + address _recipient, + address _asset, + uint256 _amount + ) external override onlyVault nonReentrant { + _withdraw(_recipient, _asset, _amount); + } + + function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + _withdraw(vaultAddress, usds, IERC20(usds).balanceOf(address(this))); + } + + function _withdraw( + address _recipient, + address _asset, + uint256 _amount + ) internal { + // You can only withdraw USDS + require(_asset == usds, "Unsupported asset"); + require(_amount > 0, "Must withdraw something"); + require(_recipient == vaultAddress, "Only the vault can withdraw"); + IERC20(usds).transfer(_recipient, _amount); + } + + function checkBalance(address _asset) + external + view + override + returns (uint256 balance) + { + if (_asset == dai) { + // Contract should not have any DAI at any point of time. + return 0; + } else if (_asset == usds) { + balance = IERC20(usds).balanceOf(address(this)); + } else { + revert("Unsupported asset"); + } + } + + function supportsAsset(address _asset) public view override returns (bool) { + return _asset == dai || _asset == usds; + } + + function collectRewardTokens() external override {} + + function _abstractSetPToken(address _asset, address _pToken) + internal + override + {} + + function safeApproveAllTokens() external override {} +} diff --git a/contracts/deploy/mainnet/124_replace_dai_with_usds.js b/contracts/deploy/mainnet/124_replace_dai_with_usds.js new file mode 100644 index 0000000000..b0a910bb64 --- /dev/null +++ b/contracts/deploy/mainnet/124_replace_dai_with_usds.js @@ -0,0 +1,230 @@ +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); +const { impersonateAndFund } = require("../../utils/signers"); +const { isFork } = require("../../utils/hardhat-helpers"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "124_replace_dai_with_usds", + reduceQueueTime: true, + // forceSkip: true, + deployerIsProposer: false, + proposalId: "", + // TODO: Temporary hack to test it on CI + simulateDirectlyOnTimelock: isFork, + }, + async ({ deployWithConfirmation, getTxOpts, withConfirmation, ethers }) => { + const DAI = addresses.mainnet.DAI; + const USDS = addresses.mainnet.USDS; + const sUSDS = addresses.mainnet.sUSDS; + + const { deployerAddr, timelockAddr, multichainStrategistAddr } = + await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const cVaultProxy = await ethers.getContract("VaultProxy"); + const cVault = await ethers.getContractAt("IVault", cVaultProxy.address); + const cDSRStrategyProxy = await ethers.getContract("MakerDsrStrategyProxy"); + + const cHarvesterProxy = await ethers.getContract("HarvesterProxy"); + const cHarvester = await ethers.getContractAt( + "Harvester", + cHarvesterProxy.address + ); + + const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); + + // 1. Deploy OracleRouter + await deployWithConfirmation("OracleRouter"); + const cOracleRouter = await ethers.getContract("OracleRouter"); + await withConfirmation( + cOracleRouter.connect(sDeployer).cacheDecimals(USDS) + ); + + // 2. Deploy Migration Strategy + const dMigrationStrategy = await deployWithConfirmation( + "DAIMigrationStrategy", + [ + { + vaultAddress: cVaultProxy.address, + platformAddress: addresses.mainnet.DaiUsdsMigrationContract, + }, + DAI, + USDS, + ] + ); + + const migrationStrategy = await ethers.getContractAt( + "DAIMigrationStrategy", + dMigrationStrategy.address + ); + + // 3. Transfer governance to timelock and initialize the contract + await withConfirmation( + migrationStrategy.connect(sDeployer).initialize(timelockAddr) + ); + + // 4. Deploy SSR Strategy + const dMakerSSRStrategyProxy = await deployWithConfirmation( + "MakerSSRStrategyProxy" + ); + const dMakerSSRStrategy = await deployWithConfirmation( + "Generalized4626Strategy", + [[sUSDS, cVaultProxy.address], USDS] + ); + const cMakerSSRStrategyProxy = await ethers.getContract( + "MakerSSRStrategyProxy" + ); + const cMakerSSRStrategy = await ethers.getContractAt( + "Generalized4626Strategy", + dMakerSSRStrategyProxy.address + ); + + // 5. Initialize the SSR Strategy + const initData = cMakerSSRStrategy.interface.encodeFunctionData( + "initialize()", + [] + ); + const initFunction = "initialize(address,address,bytes)"; + await withConfirmation( + cMakerSSRStrategyProxy.connect(sDeployer)[initFunction]( + dMakerSSRStrategy.address, + timelockAddr, // governor + initData, // data for delegate call to the initialize function on the strategy + await getTxOpts() + ) + ); + + if (isFork) { + // Withdraw the dust DAI from Morpho Aave V2 + const cMorphoAaveStrategyProxy = await ethers.getContract( + "MorphoAaveStrategyProxy" + ); + const cMorphoAaveStrategy = await ethers.getContractAt( + "MorphoAaveStrategy", + cMorphoAaveStrategyProxy.address + ); + + const daiBalance = await cMorphoAaveStrategy.checkBalance(DAI); + + const strategist = await impersonateAndFund(multichainStrategistAddr); + await cVault + .connect(strategist) + .withdrawFromStrategy( + cMorphoAaveStrategyProxy.address, + [DAI], + [daiBalance] + ); + } + + return { + name: "Replace DAI with USDS", + actions: [ + { + // Upgrade VaultAdmin implementation + // For some reason, removeAsset fails without upgrading + contract: cVault, + signature: "setAdminImpl(address)", + args: [dVaultAdmin.address], + }, + { + // Set OracleRouter on Vault + contract: cVault, + signature: "setPriceProvider(address)", + args: [cOracleRouter.address], + }, + { + // Support USDS on Vault + contract: cVault, + signature: "supportAsset(address,uint8)", + args: [USDS, 0], + }, + + { + // Approve Migration Strategy on the Vault + contract: cVault, + signature: "approveStrategy(address)", + args: [migrationStrategy.address], + }, + { + // Approve SSR Strategy on the Vault + contract: cVault, + signature: "approveStrategy(address)", + args: [cMakerSSRStrategy.address], + }, + { + // Set Migration Strategy as default for DAI + contract: cVault, + signature: "setAssetDefaultStrategy(address,address)", + args: [DAI, migrationStrategy.address], + }, + { + // Set Maker SSR Strategy as default for USDS + contract: cVault, + signature: "setAssetDefaultStrategy(address,address)", + args: [USDS, cMakerSSRStrategy.address], + }, + { + // Remove DSR strategy + // This would move all DAI to vault + contract: cVault, + signature: "removeStrategy(address)", + args: [cDSRStrategyProxy.address], + }, + { + // Allocate DAI from the Vault to the Migration Strategy + contract: cVault, + signature: "allocate()", + args: [], + }, + { + // Reset default strategy for DAI so that we can remove it + contract: cVault, + signature: "setAssetDefaultStrategy(address,address)", + args: [DAI, addresses.zero], + }, + { + // Remove Migration Strategy + // This will remove all USDS and move it to the Vault + contract: cVault, + signature: "removeStrategy(address)", + args: [migrationStrategy.address], + }, + + // { + // // Optional: Allocate USDS from the Vault to the SSR Strategy + // contract: cVault, + // signature: "allocate()", + // args: [], + // }, + + { + // Remove DAI + contract: cVault, + signature: "removeAsset(address)", + args: [DAI], + }, + + // Harvester related things + { + // Set Harvester in the SSR Strategy + contract: cMakerSSRStrategy, + signature: "setHarvesterAddress(address)", + args: [cHarvesterProxy.address], + }, + { + // Add SSR Strategy to the Harvester + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cMakerSSRStrategy.address, true], + }, + { + // Remove DSR Strategy from the Harvester + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cDSRStrategyProxy.address, false], + }, + ], + }; + } +); diff --git a/contracts/hardhat.config.js b/contracts/hardhat.config.js index 1becc79eba..cead2a6b81 100644 --- a/contracts/hardhat.config.js +++ b/contracts/hardhat.config.js @@ -373,25 +373,23 @@ module.exports = { process.env.FORK === "true" ? isHoleskyFork ? HOLESKY_DEPLOYER - : isBaseFork - ? BASE_STRATEGIST : isSonicFork ? SONIC_STRATEGIST - : MAINNET_STRATEGIST + : // Base and Eth use Multichain Strategist + MULTICHAIN_STRATEGIST : 0, hardhat: process.env.FORK === "true" ? isHoleskyFork ? HOLESKY_DEPLOYER - : isBaseFork - ? BASE_STRATEGIST : isSonicFork ? SONIC_STRATEGIST - : MAINNET_STRATEGIST + : // Base and Eth use Multichain Strategist + MULTICHAIN_STRATEGIST : 0, - mainnet: MAINNET_STRATEGIST, + mainnet: MULTICHAIN_STRATEGIST, holesky: HOLESKY_DEPLOYER, // on Holesky the deployer is also the strategist - base: BASE_STRATEGIST, + base: MULTICHAIN_STRATEGIST, sonic: SONIC_STRATEGIST, }, multichainStrategistAddr: { diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index b45088e20a..30c401221e 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -30,6 +30,8 @@ addresses.mainnet.DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; addresses.mainnet.USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; addresses.mainnet.USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; addresses.mainnet.TUSD = "0x0000000000085d4780B73119b644AE5ecd22b376"; +addresses.mainnet.USDS = "0xdc035d45d973e3ec169d2276ddab16f1e407384f"; +addresses.mainnet.sUSDS = "0xa3931d71877c0e7a3148cb7eb4463524fec27fbd"; // AAVE addresses.mainnet.AAVE_ADDRESS_PROVIDER = "0xb53c1a33016b2dc2ff3653530bff1848a515c8c5"; // v2 @@ -301,6 +303,10 @@ addresses.mainnet.validatorRegistrator = addresses.mainnet.LidoWithdrawalQueue = "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1"; +// DAI > USDS Migration Contract +addresses.mainnet.DaiUsdsMigrationContract = + "0x3225737a9bbb6473cb4a45b7244aca2befdb276a"; + // Arbitrum One addresses.arbitrumOne = {}; addresses.arbitrumOne.WOETHProxy = "0xD8724322f44E5c58D7A815F542036fb17DbbF839"; diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index f24bf2e2c7..a40e5986ca 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -1185,6 +1185,21 @@ async function handleTransitionGovernance(propDesc, propArgs) { } } +async function simulateWithTimelockImpersonation(proposal) { + log("Simulating the proposal directly on the timelock..."); + const { timelockAddr } = await getNamedAccounts(); + const timelock = await impersonateAndFund(timelockAddr); + + for (const action of proposal.actions) { + const { contract, signature, args } = action; + + log(`Sending governance action ${signature} to ${contract.address}`); + await contract.connect(timelock)[signature](...args, await getTxOpts()); + + console.log(`... ${signature} completed`); + } +} + /** * Shortcut to create a deployment on decentralized Governance (xOGN) for hardhat to use * @param {Object} options for deployment @@ -1203,6 +1218,8 @@ function deploymentWithGovernanceProposal(opts, fn) { reduceQueueTime = false, // reduce governance queue times executeGasLimit = null, skipSimulation = false, // Skips simulating execution of proposal on fork + // Simulates the actions by impersonating the timelock, helpful when debugging failing actions + simulateDirectlyOnTimelock = false, } = opts; const runDeployment = async (hre) => { const oracleAddresses = await getOracleAddresses(hre.deployments); @@ -1271,6 +1288,8 @@ function deploymentWithGovernanceProposal(opts, fn) { if (skipSimulation) { log("Building xOGN governance proposal..."); await submitProposalGnosisSafe(propArgs, propDescription, propOpts); + } else if (simulateDirectlyOnTimelock) { + await simulateWithTimelockImpersonation(proposal); } else { // On Fork we can send the proposal then impersonate the guardian to execute it. log("Sending the governance proposal to xOGN governance");