From 965d14c5d95f95ebcb7544c743d581a83eeda8cc Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 23 Jul 2024 12:37:21 +0200 Subject: [PATCH 01/33] vault manager v6 deploys v4 vaults --- contracts/SmartVaultDeployerV4.sol | 20 ++ contracts/SmartVaultManagerV6.sol | 152 +++++++++++ contracts/SmartVaultV4.sol | 242 ++++++++++++++++++ .../{ => versions}/SmartVaultDeployerV3.sol | 2 +- .../{ => versions}/SmartVaultManagerV5.sol | 2 +- contracts/{ => versions}/SmartVaultV3.sol | 0 6 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 contracts/SmartVaultDeployerV4.sol create mode 100644 contracts/SmartVaultManagerV6.sol create mode 100644 contracts/SmartVaultV4.sol rename contracts/{ => versions}/SmartVaultDeployerV3.sol (93%) rename contracts/{ => versions}/SmartVaultManagerV5.sol (99%) rename contracts/{ => versions}/SmartVaultV3.sol (100%) diff --git a/contracts/SmartVaultDeployerV4.sol b/contracts/SmartVaultDeployerV4.sol new file mode 100644 index 0000000..feca5e6 --- /dev/null +++ b/contracts/SmartVaultDeployerV4.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "contracts/SmartVaultV4.sol"; +import "contracts/PriceCalculator.sol"; +import "contracts/interfaces/ISmartVaultDeployer.sol"; + +contract SmartVaultDeployerV4 is ISmartVaultDeployer { + bytes32 private immutable NATIVE; + address private immutable priceCalculator; + + constructor(bytes32 _native, address _clEurUsd) { + NATIVE = _native; + priceCalculator = address(new PriceCalculator(_native, _clEurUsd)); + } + + function deploy(address _manager, address _owner, address _euros) external returns (address) { + return address(new SmartVaultV4(NATIVE, _manager, _owner, _euros, priceCalculator)); + } +} diff --git a/contracts/SmartVaultManagerV6.sol b/contracts/SmartVaultManagerV6.sol new file mode 100644 index 0000000..0c6c7e1 --- /dev/null +++ b/contracts/SmartVaultManagerV6.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "contracts/interfaces/INFTMetadataGenerator.sol"; +import "contracts/interfaces/IEUROs.sol"; +import "contracts/interfaces/ISmartVault.sol"; +import "contracts/interfaces/ISmartVaultDeployer.sol"; +import "contracts/interfaces/ISmartVaultIndex.sol"; +import "contracts/interfaces/ISmartVaultManager.sol"; +import "contracts/interfaces/ISmartVaultManagerV2.sol"; + +// +// TODO describe changes +// TODO upgraded zz/zz/zz +// +contract SmartVaultManagerV6 is ISmartVaultManager, ISmartVaultManagerV2, Initializable, ERC721Upgradeable, OwnableUpgradeable { + using SafeERC20 for IERC20; + + uint256 public constant HUNDRED_PC = 1e5; + + address public protocol; + address public liquidator; + address public euros; + uint256 public collateralRate; + address public tokenManager; + address public smartVaultDeployer; + ISmartVaultIndex private smartVaultIndex; + uint256 private lastToken; + address public nftMetadataGenerator; + uint256 public mintFeeRate; + uint256 public burnFeeRate; + uint256 public swapFeeRate; + address public weth; + address public swapRouter; + address public swapRouter2; + uint16 public userVaultLimit; + + event VaultDeployed(address indexed vaultAddress, address indexed owner, address vaultType, uint256 tokenId); + event VaultLiquidated(address indexed vaultAddress); + event VaultTransferred(uint256 indexed tokenId, address from, address to); + + struct SmartVaultData { + uint256 tokenId; uint256 collateralRate; uint256 mintFeeRate; + uint256 burnFeeRate; ISmartVault.Status status; + } + + function initialize() initializer public {} + + modifier onlyLiquidator { + require(msg.sender == liquidator, "err-invalid-liquidator"); + _; + } + + function vaultIDs(address _holder) public view returns (uint256[] memory) { + return smartVaultIndex.getTokenIds(_holder); + } + + function vaultData(uint256 _tokenID) external view returns (SmartVaultData memory) { + return SmartVaultData({ + tokenId: _tokenID, + collateralRate: collateralRate, + mintFeeRate: mintFeeRate, + burnFeeRate: burnFeeRate, + status: ISmartVault(smartVaultIndex.getVaultAddress(_tokenID)).status() + }); + } + + function mint() external returns (address vault, uint256 tokenId) { + tokenId = lastToken + 1; + _safeMint(msg.sender, tokenId); + lastToken = tokenId; + vault = ISmartVaultDeployer(smartVaultDeployer).deploy(address(this), msg.sender, euros); + smartVaultIndex.addVaultAddress(tokenId, payable(vault)); + IEUROs(euros).grantRole(IEUROs(euros).MINTER_ROLE(), vault); + IEUROs(euros).grantRole(IEUROs(euros).BURNER_ROLE(), vault); + emit VaultDeployed(vault, msg.sender, euros, tokenId); + } + + function liquidateVault(uint256 _tokenId) external onlyLiquidator { + ISmartVault vault = ISmartVault(smartVaultIndex.getVaultAddress(_tokenId)); + try vault.undercollateralised() returns (bool _undercollateralised) { + require(_undercollateralised, "vault-not-undercollateralised"); + vault.liquidate(); + IEUROs(euros).revokeRole(IEUROs(euros).MINTER_ROLE(), address(vault)); + IEUROs(euros).revokeRole(IEUROs(euros).BURNER_ROLE(), address(vault)); + emit VaultLiquidated(address(vault)); + } catch { + revert("other-liquidation-error"); + } + } + + function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) { + ISmartVault.Status memory vaultStatus = ISmartVault(smartVaultIndex.getVaultAddress(_tokenId)).status(); + return INFTMetadataGenerator(nftMetadataGenerator).generateNFTMetadata(_tokenId, vaultStatus); + } + + function totalSupply() external view returns (uint256) { + return lastToken; + } + + function setMintFeeRate(uint256 _rate) external onlyOwner { + mintFeeRate = _rate; + } + + function setBurnFeeRate(uint256 _rate) external onlyOwner { + burnFeeRate = _rate; + } + + function setSwapFeeRate(uint256 _rate) external onlyOwner { + swapFeeRate = _rate; + } + + function setWethAddress(address _weth) external onlyOwner() { + weth = _weth; + } + + function setSwapRouter2(address _swapRouter) external onlyOwner() { + swapRouter2 = _swapRouter; + } + + function setNFTMetadataGenerator(address _nftMetadataGenerator) external onlyOwner() { + nftMetadataGenerator = _nftMetadataGenerator; + } + + function setSmartVaultDeployer(address _smartVaultDeployer) external onlyOwner() { + smartVaultDeployer = _smartVaultDeployer; + } + + function setProtocolAddress(address _protocol) external onlyOwner() { + protocol = _protocol; + } + + function setLiquidatorAddress(address _liquidator) external onlyOwner() { + liquidator = _liquidator; + } + + function setUserVaultLimit(uint16 _userVaultLimit) external onlyOwner() { + userVaultLimit = _userVaultLimit; + } + + // TODO test transfer + function _afterTokenTransfer(address _from, address _to, uint256 _tokenId, uint256) internal override { + require(vaultIDs(_to).length < userVaultLimit, "err-vault-limit"); + smartVaultIndex.transferTokenId(_from, _to, _tokenId); + if (address(_from) != address(0)) ISmartVault(smartVaultIndex.getVaultAddress(_tokenId)).setOwner(_to); + emit VaultTransferred(_tokenId, _from, _to); + } +} \ No newline at end of file diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol new file mode 100644 index 0000000..b105ad8 --- /dev/null +++ b/contracts/SmartVaultV4.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "contracts/interfaces/IEUROs.sol"; +import "contracts/interfaces/IPriceCalculator.sol"; +import "contracts/interfaces/ISmartVault.sol"; +import "contracts/interfaces/ISmartVaultManagerV3.sol"; +import "contracts/interfaces/ISwapRouter.sol"; +import "contracts/interfaces/ITokenManager.sol"; +import "contracts/interfaces/IWETH.sol"; + +contract SmartVaultV4 is ISmartVault { + using SafeERC20 for IERC20; + + string private constant INVALID_USER = "err-invalid-user"; + string private constant UNDER_COLL = "err-under-coll"; + uint8 private constant version = 4; + bytes32 private constant vaultType = bytes32("EUROs"); + bytes32 private immutable NATIVE; + address public immutable manager; + IEUROs public immutable EUROs; + IPriceCalculator public immutable calculator; + + address public owner; + uint256 private minted; + bool private liquidated; + + event CollateralRemoved(bytes32 symbol, uint256 amount, address to); + event AssetRemoved(address token, uint256 amount, address to); + event EUROsMinted(address to, uint256 amount, uint256 fee); + event EUROsBurned(uint256 amount, uint256 fee); + + constructor(bytes32 _native, address _manager, address _owner, address _euros, address _priceCalculator) { + NATIVE = _native; + owner = _owner; + manager = _manager; + EUROs = IEUROs(_euros); + calculator = IPriceCalculator(_priceCalculator); + } + + modifier onlyVaultManager { + require(msg.sender == manager, INVALID_USER); + _; + } + + modifier onlyOwner { + require(msg.sender == owner, INVALID_USER); + _; + } + + modifier ifMinted(uint256 _amount) { + require(minted >= _amount, "err-insuff-minted"); + _; + } + + modifier ifNotLiquidated { + require(!liquidated, "err-liquidated"); + _; + } + + function getTokenManager() private view returns (ITokenManager) { + return ITokenManager(ISmartVaultManagerV3(manager).tokenManager()); + } + + function euroCollateral() private view returns (uint256 euros) { + ITokenManager tokenManager = ITokenManager(ISmartVaultManagerV3(manager).tokenManager()); + ITokenManager.Token[] memory acceptedTokens = tokenManager.getAcceptedTokens(); + for (uint256 i = 0; i < acceptedTokens.length; i++) { + ITokenManager.Token memory token = acceptedTokens[i]; + euros += calculator.tokenToEur(token, getAssetBalance(token.symbol, token.addr)); + } + } + + function maxMintable(uint256 _collateral) private view returns (uint256) { + return _collateral * ISmartVaultManagerV3(manager).HUNDRED_PC() / ISmartVaultManagerV3(manager).collateralRate(); + } + + function getAssetBalance(bytes32 _symbol, address _tokenAddress) private view returns (uint256 amount) { + return _symbol == NATIVE ? address(this).balance : IERC20(_tokenAddress).balanceOf(address(this)); + } + + function getAssets() private view returns (Asset[] memory) { + ITokenManager tokenManager = ITokenManager(ISmartVaultManagerV3(manager).tokenManager()); + ITokenManager.Token[] memory acceptedTokens = tokenManager.getAcceptedTokens(); + Asset[] memory assets = new Asset[](acceptedTokens.length); + for (uint256 i = 0; i < acceptedTokens.length; i++) { + ITokenManager.Token memory token = acceptedTokens[i]; + uint256 assetBalance = getAssetBalance(token.symbol, token.addr); + assets[i] = Asset(token, assetBalance, calculator.tokenToEur(token, assetBalance)); + } + return assets; + } + + function status() external view returns (Status memory) { + uint256 _collateral = euroCollateral(); + return Status(address(this), minted, maxMintable(_collateral), _collateral, + getAssets(), liquidated, version, vaultType); + } + + function undercollateralised() public view returns (bool) { + return minted > maxMintable(euroCollateral()); + } + + function liquidateNative() private { + if (address(this).balance != 0) { + (bool sent,) = payable(ISmartVaultManagerV3(manager).protocol()).call{value: address(this).balance}(""); + require(sent, "err-native-liquidate"); + } + } + + function liquidateERC20(IERC20 _token) private { + if (_token.balanceOf(address(this)) != 0) _token.safeTransfer(ISmartVaultManagerV3(manager).protocol(), _token.balanceOf(address(this))); + } + + function liquidate() external onlyVaultManager { + require(undercollateralised(), "err-not-liquidatable"); + liquidated = true; + minted = 0; + liquidateNative(); + ITokenManager.Token[] memory tokens = ITokenManager(ISmartVaultManagerV3(manager).tokenManager()).getAcceptedTokens(); + for (uint256 i = 0; i < tokens.length; i++) { + if (tokens[i].symbol != NATIVE) liquidateERC20(IERC20(tokens[i].addr)); + } + } + + receive() external payable {} + + function canRemoveCollateral(ITokenManager.Token memory _token, uint256 _amount) private view returns (bool) { + if (minted == 0) return true; + uint256 eurValueToRemove = calculator.tokenToEur(_token, _amount); + uint256 _newCollateral = euroCollateral() - eurValueToRemove; + return maxMintable(_newCollateral) >= minted; + } + + function removeCollateralNative(uint256 _amount, address payable _to) external onlyOwner { + require(canRemoveCollateral(getTokenManager().getToken(NATIVE), _amount), UNDER_COLL); + (bool sent,) = _to.call{value: _amount}(""); + require(sent, "err-native-call"); + emit CollateralRemoved(NATIVE, _amount, _to); + } + + function removeCollateral(bytes32 _symbol, uint256 _amount, address _to) external onlyOwner { + ITokenManager.Token memory token = getTokenManager().getToken(_symbol); + require(canRemoveCollateral(token, _amount), UNDER_COLL); + IERC20(token.addr).safeTransfer(_to, _amount); + emit CollateralRemoved(_symbol, _amount, _to); + } + + function removeAsset(address _tokenAddr, uint256 _amount, address _to) external onlyOwner { + ITokenManager.Token memory token = getTokenManager().getTokenIfExists(_tokenAddr); + if (token.addr == _tokenAddr) require(canRemoveCollateral(token, _amount), UNDER_COLL); + IERC20(_tokenAddr).safeTransfer(_to, _amount); + emit AssetRemoved(_tokenAddr, _amount, _to); + } + + function fullyCollateralised(uint256 _amount) private view returns (bool) { + return minted + _amount <= maxMintable(euroCollateral()); + } + + function mint(address _to, uint256 _amount) external onlyOwner ifNotLiquidated { + uint256 fee = _amount * ISmartVaultManagerV3(manager).mintFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC(); + require(fullyCollateralised(_amount + fee), UNDER_COLL); + minted = minted + _amount + fee; + EUROs.mint(_to, _amount); + EUROs.mint(ISmartVaultManagerV3(manager).protocol(), fee); + emit EUROsMinted(_to, _amount, fee); + } + + function burn(uint256 _amount) external ifMinted(_amount) { + uint256 fee = _amount * ISmartVaultManagerV3(manager).burnFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC(); + minted = minted - _amount; + EUROs.burn(msg.sender, _amount + fee); + if (fee > 0) EUROs.mint(ISmartVaultManagerV3(manager).protocol(), fee); + emit EUROsBurned(_amount, fee); + } + + + function getToken(bytes32 _symbol) private view returns (ITokenManager.Token memory _token) { + ITokenManager.Token[] memory tokens = ITokenManager(ISmartVaultManagerV3(manager).tokenManager()).getAcceptedTokens(); + for (uint256 i = 0; i < tokens.length; i++) { + if (tokens[i].symbol == _symbol) _token = tokens[i]; + } + require(_token.symbol != bytes32(0), "err-invalid-swap"); + } + + function getSwapAddressFor(bytes32 _symbol) private view returns (address) { + ITokenManager.Token memory _token = getToken(_symbol); + return _token.addr == address(0) ? ISmartVaultManagerV3(manager).weth() : _token.addr; + } + + function executeNativeSwapAndFee(ISwapRouter.ExactInputSingleParams memory _params, uint256 _swapFee) private { + (bool sent,) = payable(ISmartVaultManagerV3(manager).protocol()).call{value: _swapFee}(""); + require(sent, "err-swap-fee-native"); + ISwapRouter(ISmartVaultManagerV3(manager).swapRouter2()).exactInputSingle{value: _params.amountIn}(_params); + } + + function executeERC20SwapAndFee(ISwapRouter.ExactInputSingleParams memory _params, uint256 _swapFee) private { + IERC20(_params.tokenIn).safeTransfer(ISmartVaultManagerV3(manager).protocol(), _swapFee); + IERC20(_params.tokenIn).safeApprove(ISmartVaultManagerV3(manager).swapRouter2(), _params.amountIn); + ISwapRouter(ISmartVaultManagerV3(manager).swapRouter2()).exactInputSingle(_params); + IERC20(_params.tokenIn).safeApprove(ISmartVaultManagerV3(manager).swapRouter2(), 0); + IWETH weth = IWETH(ISmartVaultManagerV3(manager).weth()); + // convert potentially received weth to eth + uint256 wethBalance = weth.balanceOf(address(this)); + if (wethBalance > 0) weth.withdraw(wethBalance); + } + + function calculateMinimumAmountOut(bytes32 _inTokenSymbol, bytes32 _outTokenSymbol, uint256 _amount) private view returns (uint256) { + ISmartVaultManagerV3 _manager = ISmartVaultManagerV3(manager); + uint256 requiredCollateralValue = minted * _manager.collateralRate() / _manager.HUNDRED_PC(); + // add 1% min collateral buffer + uint256 collateralValueMinusSwapValue = euroCollateral() - calculator.tokenToEur(getToken(_inTokenSymbol), _amount * 101 / 100); + return collateralValueMinusSwapValue >= requiredCollateralValue ? + 0 : calculator.eurToToken(getToken(_outTokenSymbol), requiredCollateralValue - collateralValueMinusSwapValue); + } + + function swap(bytes32 _inToken, bytes32 _outToken, uint256 _amount, uint256 _requestedMinOut) external onlyOwner { + uint256 swapFee = _amount * ISmartVaultManagerV3(manager).swapFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC(); + address inToken = getSwapAddressFor(_inToken); + uint256 minimumAmountOut = calculateMinimumAmountOut(_inToken, _outToken, _amount + swapFee); + if (_requestedMinOut > minimumAmountOut) minimumAmountOut = _requestedMinOut; + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: inToken, + tokenOut: getSwapAddressFor(_outToken), + fee: 3000, + recipient: address(this), + deadline: block.timestamp + 60, + amountIn: _amount - swapFee, + amountOutMinimum: minimumAmountOut, + sqrtPriceLimitX96: 0 + }); + inToken == ISmartVaultManagerV3(manager).weth() ? + executeNativeSwapAndFee(params, swapFee) : + executeERC20SwapAndFee(params, swapFee); + } + + function setOwner(address _newOwner) external onlyVaultManager { + owner = _newOwner; + } +} diff --git a/contracts/SmartVaultDeployerV3.sol b/contracts/versions/SmartVaultDeployerV3.sol similarity index 93% rename from contracts/SmartVaultDeployerV3.sol rename to contracts/versions/SmartVaultDeployerV3.sol index 77251f8..cdb3b3c 100644 --- a/contracts/SmartVaultDeployerV3.sol +++ b/contracts/versions/SmartVaultDeployerV3.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; -import "contracts/SmartVaultV3.sol"; +import "contracts/versions/SmartVaultV3.sol"; import "contracts/PriceCalculator.sol"; import "contracts/interfaces/ISmartVaultDeployer.sol"; diff --git a/contracts/SmartVaultManagerV5.sol b/contracts/versions/SmartVaultManagerV5.sol similarity index 99% rename from contracts/SmartVaultManagerV5.sol rename to contracts/versions/SmartVaultManagerV5.sol index 7b149bd..c105054 100644 --- a/contracts/SmartVaultManagerV5.sol +++ b/contracts/versions/SmartVaultManagerV5.sol @@ -16,7 +16,7 @@ import "contracts/interfaces/ISmartVaultManagerV2.sol"; // // allows use of different swap router address (post 7/11 attack) // allows setting of protocol wallet address + liquidator address -// upgraded zz/zz/zz +// upgraded 22/02/24 // contract SmartVaultManagerV5 is ISmartVaultManager, ISmartVaultManagerV2, Initializable, ERC721Upgradeable, OwnableUpgradeable { using SafeERC20 for IERC20; diff --git a/contracts/SmartVaultV3.sol b/contracts/versions/SmartVaultV3.sol similarity index 100% rename from contracts/SmartVaultV3.sol rename to contracts/versions/SmartVaultV3.sol From ed06e2dfcc65fbb0b54ef2e9f36e27d84486e741 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 23 Jul 2024 12:37:49 +0200 Subject: [PATCH 02/33] update test with v4 vaults, wip test for vault yield --- test/SmartVault.js | 8 +++++++- test/common.js | 13 +++++++------ test/smartVaultManager.js | 9 ++++++++- test/svgGenerator.js | 2 +- test/versioning.js | 6 +++--- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/test/SmartVault.js b/test/SmartVault.js index 58d069d..d51e4a0 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -15,7 +15,7 @@ describe('SmartVault', async () => { await ClEurUsd.setPrice(DEFAULT_EUR_USD_PRICE); EUROs = await (await ethers.getContractFactory('EUROsMock')).deploy(); TokenManager = await (await ethers.getContractFactory('TokenManager')).deploy(ETH, ClEthUsd.address); - const SmartVaultDeployer = await (await ethers.getContractFactory('SmartVaultDeployerV3')).deploy(ETH, ClEurUsd.address); + const SmartVaultDeployer = await (await ethers.getContractFactory('SmartVaultDeployerV4')).deploy(ETH, ClEurUsd.address); const SmartVaultIndex = await (await ethers.getContractFactory('SmartVaultIndex')).deploy(); const NFTMetadataGenerator = await (await getNFTMetadataContract()).deploy(); MockSwapRouter = await (await ethers.getContractFactory('MockSwapRouter')).deploy(); @@ -436,4 +436,10 @@ describe('SmartVault', async () => { expect(await Stablecoin.balanceOf(protocol.address)).to.equal(swapFee); }); }); + + describe('yield', async () => { + it('puts all of given collateral asset into yield', async () => { + + }); + }); }); \ No newline at end of file diff --git a/test/common.js b/test/common.js index 689f2b7..f3ca82e 100644 --- a/test/common.js +++ b/test/common.js @@ -39,13 +39,14 @@ const fullyUpgradedSmartVaultManager = async ( await upgrades.upgradeProxy(v1.address, await ethers.getContractFactory('SmartVaultManagerV2')); await upgrades.upgradeProxy(v1.address, await ethers.getContractFactory('SmartVaultManagerV3')); await upgrades.upgradeProxy(v1.address, await ethers.getContractFactory('SmartVaultManagerV4')); - const V5 = await upgrades.upgradeProxy(v1.address, await ethers.getContractFactory('SmartVaultManagerV5')); + await upgrades.upgradeProxy(v1.address, await ethers.getContractFactory('SmartVaultManagerV5')); + const V6 = await upgrades.upgradeProxy(v1.address, await ethers.getContractFactory('SmartVaultManagerV6')); - await V5.setSwapFeeRate(protocolFeeRate); - await V5.setWethAddress(wethAddress); - await V5.setSwapRouter2(swapRouterAddress); - await V5.setUserVaultLimit(vaultLimit); - return V5; + await V6.setSwapFeeRate(protocolFeeRate); + await V6.setWethAddress(wethAddress); + await V6.setSwapRouter2(swapRouterAddress); + await V6.setUserVaultLimit(vaultLimit); + return V6; } module.exports = { diff --git a/test/smartVaultManager.js b/test/smartVaultManager.js index aef7278..4318036 100644 --- a/test/smartVaultManager.js +++ b/test/smartVaultManager.js @@ -19,7 +19,7 @@ describe('SmartVaultManager', async () => { TokenManager = await (await ethers.getContractFactory('TokenManager')).deploy(ETH, ClEthUsd.address); EUROs = await (await ethers.getContractFactory('EUROsMock')).deploy(); Tether = await (await ethers.getContractFactory('ERC20Mock')).deploy('Tether', 'USDT', 6); - SmartVaultDeployer = await (await ethers.getContractFactory('SmartVaultDeployerV3')).deploy(ETH, ClEurUsd.address); + SmartVaultDeployer = await (await ethers.getContractFactory('SmartVaultDeployerV4')).deploy(ETH, ClEurUsd.address); const SmartVaultIndex = await (await ethers.getContractFactory('SmartVaultIndex')).deploy(); MockSwapRouter = await (await ethers.getContractFactory('MockSwapRouter')).deploy(); NFTMetadataGenerator = await (await getNFTMetadataContract()).deploy(); @@ -215,5 +215,12 @@ describe('SmartVaultManager', async () => { expect(metadataJSON).to.have.string('base64'); }); }); + + describe('vault version', async () => { + it('deploys v4 vaults', async () => { + const vault = await ethers.getContractAt('SmartVault', vaultAddress); + expect((await vault.status()).version).to.equal(4); + }); + }); }); }); \ No newline at end of file diff --git a/test/svgGenerator.js b/test/svgGenerator.js index 4a859e4..ded8f99 100644 --- a/test/svgGenerator.js +++ b/test/svgGenerator.js @@ -16,7 +16,7 @@ const getSvgMintConfig = (minted, maxMintable, totalCollateralValue) => { describe('SVG Generator', async () => { // uncomment to show svg - let printViewableSvgInTest = true; + let printViewableSvgInTest = false; let svgGenerator; beforeEach(async () => { diff --git a/test/versioning.js b/test/versioning.js index 65b7bf2..652b5bf 100644 --- a/test/versioning.js +++ b/test/versioning.js @@ -29,13 +29,13 @@ describe('Contract Versioning', async () => { expect(v1Vault.status.vaultType).to.equal(ethers.utils.formatBytes32String('EUROs')); // version smart vault manager, to deploy v3 with different vaults - const VaultDeployerV3 = await (await ethers.getContractFactory('TestSmartVaultDeployerV2')).deploy(ETH, ClEurUsd.address); + const VaultDeployerV2 = await (await ethers.getContractFactory('TestSmartVaultDeployerV2')).deploy(ETH, ClEurUsd.address); const TokenManagerV2 = await (await ethers.getContractFactory('TokenManager')).deploy(ETH, ClEthUsd.address); // try upgrading with non-owner let upgrade = upgrades.upgradeProxy(VaultManagerV1.address, await ethers.getContractFactory('TestSmartVaultManagerV2', user), { - call: {fn: 'completeUpgrade', args: [VaultDeployerV3.address]} + call: {fn: 'completeUpgrade', args: [VaultDeployerV2.address]} } ); @@ -43,7 +43,7 @@ describe('Contract Versioning', async () => { upgrade = upgrades.upgradeProxy(VaultManagerV1.address, await ethers.getContractFactory('TestSmartVaultManagerV2'), { - call: {fn: 'completeUpgrade', args: [VaultDeployerV3.address]} + call: {fn: 'completeUpgrade', args: [VaultDeployerV2.address]} } ); From dd51886c2bb0cc38f80cde66428978fd71acbe8e Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 23 Jul 2024 13:02:48 +0200 Subject: [PATCH 03/33] vault deposits eth to smart vault yield manager when yield deposited --- contracts/SmartVaultManagerV6.sol | 5 +++++ contracts/SmartVaultV4.sol | 5 +++++ contracts/SmartVaultYieldManager.sol | 11 +++++++++++ contracts/interfaces/ISmartVaultManagerV3.sol | 1 + contracts/interfaces/ISmartVaultYieldManager.sol | 6 ++++++ test/SmartVault.js | 16 +++++++++++++--- test/common.js | 3 ++- 7 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 contracts/SmartVaultYieldManager.sol create mode 100644 contracts/interfaces/ISmartVaultYieldManager.sol diff --git a/contracts/SmartVaultManagerV6.sol b/contracts/SmartVaultManagerV6.sol index 0c6c7e1..4f9a0d9 100644 --- a/contracts/SmartVaultManagerV6.sol +++ b/contracts/SmartVaultManagerV6.sol @@ -38,6 +38,7 @@ contract SmartVaultManagerV6 is ISmartVaultManager, ISmartVaultManagerV2, Initia address public swapRouter; address public swapRouter2; uint16 public userVaultLimit; + address public yieldManager; event VaultDeployed(address indexed vaultAddress, address indexed owner, address vaultType, uint256 tokenId); event VaultLiquidated(address indexed vaultAddress); @@ -142,6 +143,10 @@ contract SmartVaultManagerV6 is ISmartVaultManager, ISmartVaultManagerV2, Initia userVaultLimit = _userVaultLimit; } + function setYieldManager(address _yieldManager) external onlyOwner() { + yieldManager = _yieldManager; + } + // TODO test transfer function _afterTokenTransfer(address _from, address _to, uint256 _tokenId, uint256) internal override { require(vaultIDs(_to).length < userVaultLimit, "err-vault-limit"); diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index b105ad8..a6ab189 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -7,6 +7,7 @@ import "contracts/interfaces/IEUROs.sol"; import "contracts/interfaces/IPriceCalculator.sol"; import "contracts/interfaces/ISmartVault.sol"; import "contracts/interfaces/ISmartVaultManagerV3.sol"; +import "contracts/interfaces/ISmartVaultYieldManager.sol"; import "contracts/interfaces/ISwapRouter.sol"; import "contracts/interfaces/ITokenManager.sol"; import "contracts/interfaces/IWETH.sol"; @@ -236,6 +237,10 @@ contract SmartVaultV4 is ISmartVault { executeERC20SwapAndFee(params, swapFee); } + function depositYield(bytes32 _symbol) external { + ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield{value: address(this).balance}(_symbol); + } + function setOwner(address _newOwner) external onlyVaultManager { owner = _newOwner; } diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol new file mode 100644 index 0000000..a7582fd --- /dev/null +++ b/contracts/SmartVaultYieldManager.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "contracts/interfaces/ISmartVaultYieldManager.sol"; + +contract SmartVaultYieldManager is ISmartVaultYieldManager { + + function depositYield(bytes32 _symbol) external payable { + + } +} diff --git a/contracts/interfaces/ISmartVaultManagerV3.sol b/contracts/interfaces/ISmartVaultManagerV3.sol index 1942fbf..383c650 100644 --- a/contracts/interfaces/ISmartVaultManagerV3.sol +++ b/contracts/interfaces/ISmartVaultManagerV3.sol @@ -6,4 +6,5 @@ import "contracts/interfaces/ISmartVaultManagerV2.sol"; interface ISmartVaultManagerV3 is ISmartVaultManagerV2, ISmartVaultManager { function swapRouter2() external view returns (address); + function yieldManager() external view returns (address); } \ No newline at end of file diff --git a/contracts/interfaces/ISmartVaultYieldManager.sol b/contracts/interfaces/ISmartVaultYieldManager.sol new file mode 100644 index 0000000..3dd5984 --- /dev/null +++ b/contracts/interfaces/ISmartVaultYieldManager.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +interface ISmartVaultYieldManager { + function depositYield(bytes32 _symbol) external payable; +} \ No newline at end of file diff --git a/test/SmartVault.js b/test/SmartVault.js index d51e4a0..e1acc6f 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -20,11 +20,12 @@ describe('SmartVault', async () => { const NFTMetadataGenerator = await (await getNFTMetadataContract()).deploy(); MockSwapRouter = await (await ethers.getContractFactory('MockSwapRouter')).deploy(); MockWeth = await (await ethers.getContractFactory('MockWETH')).deploy(); + const YieldManager = await (await ethers.getContractFactory('SmartVaultYieldManager')).deploy(); VaultManager = await fullyUpgradedSmartVaultManager( DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, EUROs.address, protocol.address, protocol.address, TokenManager.address, SmartVaultDeployer.address, SmartVaultIndex.address, NFTMetadataGenerator.address, MockWeth.address, - MockSwapRouter.address, TEST_VAULT_LIMIT + MockSwapRouter.address, TEST_VAULT_LIMIT, YieldManager.address ); await SmartVaultIndex.setVaultManager(VaultManager.address); await EUROs.grantRole(await EUROs.DEFAULT_ADMIN_ROLE(), VaultManager.address); @@ -32,7 +33,7 @@ describe('SmartVault', async () => { const [ vaultID ] = await VaultManager.vaultIDs(user.address); const { status } = await VaultManager.vaultData(vaultID); const { vaultAddress } = status; - Vault = await ethers.getContractAt('SmartVaultV3', vaultAddress); + Vault = await ethers.getContractAt('SmartVaultV4', vaultAddress); }); describe('ownership', async () => { @@ -437,9 +438,18 @@ describe('SmartVault', async () => { }); }); - describe('yield', async () => { + describe.only('yield', async () => { it('puts all of given collateral asset into yield', async () => { + const ethCollateral = ethers.utils.parseEther('0.1') + await user.sendTransaction({ to: Vault.address, value: ethCollateral }); + + let { collateral, totalCollateralValue } = await Vault.status(); + expect(getCollateralOf('ETH', collateral).amount).to.equal(ethCollateral); + await Vault.depositYield(ETH); + ({ collateral, totalCollateralValue } = await Vault.status()); + expect(getCollateralOf('ETH', collateral).amount).to.equal(0); + expect(totalCollateralValue).to.be.greaterThan(0); }); }); }); \ No newline at end of file diff --git a/test/common.js b/test/common.js index f3ca82e..fda1ac7 100644 --- a/test/common.js +++ b/test/common.js @@ -27,7 +27,7 @@ const fullyUpgradedSmartVaultManager = async ( collateralRate, protocolFeeRate, eurosAddress, protocolAddress, liquidatorAddress, tokenManagerAddress, smartVaultDeployerAddress, smartVaultIndexAddress, nFTMetadataGeneratorAddress, wethAddress, - swapRouterAddress, vaultLimit + swapRouterAddress, vaultLimit, yieldManagerAddress ) => { const v1 = await upgrades.deployProxy(await ethers.getContractFactory('SmartVaultManager'), [ collateralRate, protocolFeeRate, eurosAddress, protocolAddress, @@ -45,6 +45,7 @@ const fullyUpgradedSmartVaultManager = async ( await V6.setSwapFeeRate(protocolFeeRate); await V6.setWethAddress(wethAddress); await V6.setSwapRouter2(swapRouterAddress); + await V6.setYieldManager(yieldManagerAddress); await V6.setUserVaultLimit(vaultLimit); return V6; } From c24dcfe101108800713cd186f4ef2fc8ef51a8d4 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 24 Jul 2024 10:50:30 +0200 Subject: [PATCH 04/33] test that euro collateral remains the same after swap --- test/SmartVault.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/SmartVault.js b/test/SmartVault.js index e1acc6f..bbc7955 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -444,12 +444,13 @@ describe('SmartVault', async () => { await user.sendTransaction({ to: Vault.address, value: ethCollateral }); let { collateral, totalCollateralValue } = await Vault.status(); + const preYieldCollateral = totalCollateralValue; expect(getCollateralOf('ETH', collateral).amount).to.equal(ethCollateral); - await Vault.depositYield(ETH); + await Vault.depositYield(ETH, HUNDRED_PC.div(2)); ({ collateral, totalCollateralValue } = await Vault.status()); expect(getCollateralOf('ETH', collateral).amount).to.equal(0); - expect(totalCollateralValue).to.be.greaterThan(0); + expect(totalCollateralValue).to.eq(preYieldCollateral); }); }); }); \ No newline at end of file From e6ef611d7cfd769e02f87f99fd33cd16bd6e1fe9 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 24 Jul 2024 10:51:03 +0200 Subject: [PATCH 05/33] pass euro percentage of yield into yield manager --- contracts/SmartVaultV4.sol | 4 ++-- contracts/interfaces/ISmartVaultYieldManager.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index a6ab189..1a5d2cd 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -237,8 +237,8 @@ contract SmartVaultV4 is ISmartVault { executeERC20SwapAndFee(params, swapFee); } - function depositYield(bytes32 _symbol) external { - ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield{value: address(this).balance}(_symbol); + function depositYield(bytes32 _symbol, uint256 _euroPercentage) external { + ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield{value: address(this).balance}(_symbol, _euroPercentage); } function setOwner(address _newOwner) external onlyVaultManager { diff --git a/contracts/interfaces/ISmartVaultYieldManager.sol b/contracts/interfaces/ISmartVaultYieldManager.sol index 3dd5984..800917d 100644 --- a/contracts/interfaces/ISmartVaultYieldManager.sol +++ b/contracts/interfaces/ISmartVaultYieldManager.sol @@ -2,5 +2,5 @@ pragma solidity 0.8.17; interface ISmartVaultYieldManager { - function depositYield(bytes32 _symbol) external payable; + function depositYield(bytes32 _symbol, uint256 _euroPercentage) external payable; } \ No newline at end of file From 13a0777855cb681698b331ce8cb4e9aa87085abc Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 24 Jul 2024 10:51:17 +0200 Subject: [PATCH 06/33] pseudo code wip of yield --- contracts/SmartVaultYieldManager.sol | 36 +++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index a7582fd..ceb60f2 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -2,10 +2,44 @@ pragma solidity 0.8.17; import "contracts/interfaces/ISmartVaultYieldManager.sol"; +import "contracts/interfaces/IWETH.sol"; contract SmartVaultYieldManager is ISmartVaultYieldManager { - function depositYield(bytes32 _symbol) external payable { + mapping(bytes32 => address) private vaults; + address private eurosVault; + function swap(bytes32 _collateralSymbol, address _tokenOut, uint256 _amountIn) private { + address _tokenIn = ITokenManager(tokenManager).getToken(_collateralSymbol).addr; + if (_collateralSymbol == bytes32("ETH")) { + IWETH(WETH).deposit{ value: _amountIn }(); + _tokenIn = WETH; + }; + ISwapRouter(swapRouter).exactInputSingle({ + tokenIn: _tokenIn, + tokenOut: _tokenOut, + fee: 3000, + recipient: address(this), + deadline: block.timestamp + 60, + amountIn: _amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + } + + function swapAndDeposit(bytes32 _collateralSymbol, address _vault, uint256 _toSwap) private { + address _token0 = IHypervisor(_vault).token0; + address _token1 = IHypervisor(_vault).token1; + swap(_collateralSymbol, _token0, _toSwap / 2); + swap(_collateralSymbol, _token1, _toSwap / 2); + } + + function depositYield(bytes32 _collateralSymbol, uint256 _euroPercentage) external payable { + uint256 _balance = _collateralSymbol == bytes32("ETH") ? + address(this).balance : + IERC20(ITokenManager(tokenManager).getToken(_collateralSymbol).addr).balanceOf(address(this)); + uint256 _euroPortion = _euroPercentage * _balance / 1e5; + swapAndDeposit(_collateralSymbol, eurosVault, _euroPortion); + swapAndDeposit(_collateralSymbol, vaults[_collateralSymbol], _balance - _euroPortion); } } From d917edfeb1c29cb7803635b9fc4c63499a45633e Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 7 Aug 2024 14:30:14 +0200 Subject: [PATCH 07/33] withdraw and collateral calculation wip --- contracts/SmartVaultV4.sol | 42 ++++- contracts/SmartVaultYieldManager.sol | 152 ++++++++++++++---- contracts/interfaces/IHypervisor.sol | 17 ++ .../interfaces/ISmartVaultYieldManager.sol | 2 +- contracts/interfaces/ISwapRouter.sol | 33 ++++ contracts/interfaces/IUniProxy.sol | 7 + contracts/interfaces/IWETH.sol | 1 + contracts/test_utils/GammaVaultMock.sol | 34 ++++ contracts/test_utils/MockSwapRouter.sol | 10 ++ contracts/test_utils/MockWETH.sol | 9 +- contracts/test_utils/UniProxyMock.sol | 14 ++ scripts/liquidate.js | 13 ++ scripts/ratio.js | 59 +++++++ test/SmartVault.js | 25 ++- 14 files changed, 377 insertions(+), 41 deletions(-) create mode 100644 contracts/interfaces/IHypervisor.sol create mode 100644 contracts/interfaces/IUniProxy.sol create mode 100644 contracts/test_utils/GammaVaultMock.sol create mode 100644 contracts/test_utils/UniProxyMock.sol create mode 100644 scripts/liquidate.js create mode 100644 scripts/ratio.js diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index 1a5d2cd..c2a43ec 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.17; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "contracts/interfaces/IEUROs.sol"; +import "contracts/interfaces/IHypervisor.sol"; import "contracts/interfaces/IPriceCalculator.sol"; import "contracts/interfaces/ISmartVault.sol"; import "contracts/interfaces/ISmartVaultManagerV3.sol"; @@ -23,6 +24,7 @@ contract SmartVaultV4 is ISmartVault { address public immutable manager; IEUROs public immutable EUROs; IPriceCalculator public immutable calculator; + address[] private vaultTokens; address public owner; uint256 private minted; @@ -65,13 +67,43 @@ contract SmartVaultV4 is ISmartVault { return ITokenManager(ISmartVaultManagerV3(manager).tokenManager()); } + function yieldVaultCollateral(ITokenManager.Token[] memory _acceptedTokens) private view returns (uint256 _euros) { + for (uint256 i = 0; i < vaultTokens.length; i++) { + IHypervisor _vaultToken = IHypervisor(vaultTokens[i]); + uint256 _balance = _vaultToken.balanceOf(address(this)); + if (_balance > 0) { + uint256 _totalSupply = _vaultToken.totalSupply(); + (uint256 _underlyingTotal0, uint256 _underlyingTotal1) = _vaultToken.getTotalAmounts(); + address _token0 = _vaultToken.token0(); + address _token1 = _vaultToken.token1(); + uint256 _underlying0 = _balance * _underlyingTotal0 / _totalSupply; + uint256 _underlying1 = _balance * _underlyingTotal1 / _totalSupply; + if (_token0 == address(EUROs) || _token1 == address(EUROs)) { + // both EUROs and its vault pair are € stablecoins, but can be equivalent to €1 in collateral + _euros += _underlying0; + _euros += _underlying1; + } else { + // TODO how do we deal with WETH as underlying token? + // add WETH as collateral? or check for it here? + for (uint256 j = 0; i < _acceptedTokens.length; j++) { + ITokenManager.Token memory _token = _acceptedTokens[j]; + if (_token.addr == _token0) _euros += calculator.tokenToEur(_token, _underlying0); + if (_token.addr == _token1) _euros += calculator.tokenToEur(_token, _underlying1); + } + } + } + } + } + function euroCollateral() private view returns (uint256 euros) { ITokenManager tokenManager = ITokenManager(ISmartVaultManagerV3(manager).tokenManager()); ITokenManager.Token[] memory acceptedTokens = tokenManager.getAcceptedTokens(); for (uint256 i = 0; i < acceptedTokens.length; i++) { - ITokenManager.Token memory token = acceptedTokens[i]; - euros += calculator.tokenToEur(token, getAssetBalance(token.symbol, token.addr)); + ITokenManager.Token memory _token = acceptedTokens[i]; + euros += calculator.tokenToEur(_token, getAssetBalance(_token.symbol, _token.addr)); } + + euros += yieldVaultCollateral(acceptedTokens); } function maxMintable(uint256 _collateral) private view returns (uint256) { @@ -238,7 +270,11 @@ contract SmartVaultV4 is ISmartVault { } function depositYield(bytes32 _symbol, uint256 _euroPercentage) external { - ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield{value: address(this).balance}(_symbol, _euroPercentage); + ITokenManager.Token memory _token = getTokenManager().getToken(_symbol); + uint256 _balance = getAssetBalance(_symbol, _token.addr); + (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield{value: address(this).balance}(_token.addr, _euroPercentage); + vaultTokens.push(_vault1); + vaultTokens.push(_vault2); } function setOwner(address _newOwner) external onlyVaultManager { diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index ceb60f2..62368d6 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -1,45 +1,131 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; +import "contracts/interfaces/IHypervisor.sol"; import "contracts/interfaces/ISmartVaultYieldManager.sol"; +import "contracts/interfaces/ISwapRouter.sol"; +import "contracts/interfaces/IUniProxy.sol"; import "contracts/interfaces/IWETH.sol"; contract SmartVaultYieldManager is ISmartVaultYieldManager { + address private immutable EUROs; + address private immutable EURA; + address private immutable WETH; + address private immutable uniProxy; + address private immutable eurosRouter; + address private immutable euroVault; + address private immutable uniswapRouter; + uint256 private constant HUNDRED_PC = 1e5; + mapping(address => VaultData) private vaultData; - mapping(bytes32 => address) private vaults; - address private eurosVault; - - function swap(bytes32 _collateralSymbol, address _tokenOut, uint256 _amountIn) private { - address _tokenIn = ITokenManager(tokenManager).getToken(_collateralSymbol).addr; - if (_collateralSymbol == bytes32("ETH")) { - IWETH(WETH).deposit{ value: _amountIn }(); - _tokenIn = WETH; - }; - ISwapRouter(swapRouter).exactInputSingle({ - tokenIn: _tokenIn, - tokenOut: _tokenOut, - fee: 3000, + struct VaultData { address vaultAddr; uint24 poolFee; bytes pathToEURA; } + + constructor(address _EUROs, address _EURA, address _WETH, address _uniProxy, address _eurosRouter, address _euroVault, address _uniswapRouter) { + EUROs = _EUROs; + EURA = _EURA; + WETH = _WETH; + uniProxy = _uniProxy; + eurosRouter = _eurosRouter; + euroVault = _euroVault; + uniswapRouter = _uniswapRouter; + } + + function balance(address _token) private view returns (uint256) { + return IERC20(_token).balanceOf(address(this)); + } + + function swapToRatio(address _tokenA, address _hypervisor, address _swapRouter, uint24 _fee) private { + address _tokenB = _tokenA == IHypervisor(_hypervisor).token0() ? + IHypervisor(_hypervisor).token1() : IHypervisor(_hypervisor).token0(); + uint256 _tokenBBalance = balance(_tokenB); + (uint256 amountStart, uint256 amountEnd) = IUniProxy(uniProxy).getDepositAmount(_hypervisor, _tokenA, balance(_tokenA)); + uint256 _divisor = 2; + bool _tokenBTooLarge; + while(_tokenBBalance < amountStart || _tokenBBalance > amountEnd) { + uint256 _midRatio = (amountStart + amountEnd) / 2; + if (_tokenBBalance < _midRatio) { + if (_tokenBTooLarge) { + _divisor++; + _tokenBTooLarge = false; + } + IERC20(_tokenA).approve(_swapRouter, balance(_tokenA)); + try ISwapRouter(_swapRouter).exactOutputSingle(ISwapRouter.ExactOutputSingleParams({ + tokenIn: _tokenA, + tokenOut: _tokenB, + fee: _fee, + recipient: address(this), + deadline: block.timestamp + 60, + amountOut: (_midRatio - _tokenBBalance) / _divisor, + amountInMaximum: balance(_tokenA), + sqrtPriceLimitX96: 0 + })) returns (uint256) {} catch { + _divisor++; + } + } else { + if (!_tokenBTooLarge) { + _divisor++; + _tokenBTooLarge = true; + } + IERC20(_tokenB).approve(_swapRouter, (_tokenBBalance - _midRatio) / _divisor); + try ISwapRouter(_swapRouter).exactInputSingle(ISwapRouter.ExactInputSingleParams({ + tokenIn: _tokenB, + tokenOut: _tokenA, + fee: _fee, + recipient: address(this), + deadline: block.timestamp + 60, + amountIn: (_tokenBBalance - _midRatio) / _divisor, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + })) returns (uint256) {} catch { + _divisor++; + } + } + _tokenBBalance = balance(_tokenB); + (amountStart, amountEnd) = IUniProxy(uniProxy).getDepositAmount(_hypervisor, _tokenA, balance(_tokenA)); + } + } + + function swapToEURA(address _collateralToken, uint256 _euroPercentage) private { + uint256 _euroYieldPortion = balance(_collateralToken) * _euroPercentage / HUNDRED_PC; + IERC20(_collateralToken).approve(eurosRouter, _euroYieldPortion); + ISwapRouter(eurosRouter).exactInput(ISwapRouter.ExactInputParams({ + path: vaultData[_collateralToken].pathToEURA, recipient: address(this), deadline: block.timestamp + 60, - amountIn: _amountIn, - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }); - } - - function swapAndDeposit(bytes32 _collateralSymbol, address _vault, uint256 _toSwap) private { - address _token0 = IHypervisor(_vault).token0; - address _token1 = IHypervisor(_vault).token1; - swap(_collateralSymbol, _token0, _toSwap / 2); - swap(_collateralSymbol, _token1, _toSwap / 2); - } - - function depositYield(bytes32 _collateralSymbol, uint256 _euroPercentage) external payable { - uint256 _balance = _collateralSymbol == bytes32("ETH") ? - address(this).balance : - IERC20(ITokenManager(tokenManager).getToken(_collateralSymbol).addr).balanceOf(address(this)); - uint256 _euroPortion = _euroPercentage * _balance / 1e5; - swapAndDeposit(_collateralSymbol, eurosVault, _euroPortion); - swapAndDeposit(_collateralSymbol, vaults[_collateralSymbol], _balance - _euroPortion); + amountIn: _euroYieldPortion, + amountOutMinimum: 1 + })); + } + + function deposit(address _vault) private { + address _token0 = IHypervisor(_vault).token0(); + address _token1 = IHypervisor(_vault).token1(); + IERC20(_token0).approve(_vault, balance(_token0)); + IERC20(_token1).approve(_vault, balance(_token1)); + IUniProxy(uniProxy).deposit(balance(_token0), balance(_token1), msg.sender, _vault, [uint256(0),uint256(0),uint256(0),uint256(0)]); + } + + function euroDeposit(address _collateralToken, uint256 _euroPercentage) private { + swapToEURA(_collateralToken, _euroPercentage); + swapToRatio(EURA, euroVault, eurosRouter, 500); + deposit(euroVault); + } + + function otherDeposit(address _collateralToken, VaultData memory _vaultData) private { + swapToRatio(_collateralToken, _vaultData.vaultAddr, uniswapRouter, _vaultData.poolFee); + deposit(_vaultData.vaultAddr); + } + + function depositYield(address _collateralToken, uint256 _euroPercentage) external payable returns (address _vault0, address _vault1) { + if (_collateralToken == address(0)) _collateralToken = WETH; + IWETH(WETH).deposit{value: msg.value}(); + euroDeposit(_collateralToken, _euroPercentage); + VaultData memory _vaultData = vaultData[_collateralToken]; + otherDeposit(_collateralToken, _vaultData); + return (euroVault, _vaultData.vaultAddr); + } + + function addVaultData(address _collateralToken, address _vaultAddr, uint24 _poolFee, bytes memory _EURASwapPath) external { + vaultData[_collateralToken] = VaultData(_vaultAddr, _poolFee, _EURASwapPath); } } diff --git a/contracts/interfaces/IHypervisor.sol b/contracts/interfaces/IHypervisor.sol new file mode 100644 index 0000000..83a7a57 --- /dev/null +++ b/contracts/interfaces/IHypervisor.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IHypervisor is IERC20 { + function token0() external view returns (address); + function token1() external view returns (address); + function getTotalAmounts() external view returns (uint256 total0, uint256 total1); + function deposit( + uint256 deposit0, + uint256 deposit1, + address to, + address from, + uint256[4] memory inMin + ) external returns (uint256 shares); +} \ No newline at end of file diff --git a/contracts/interfaces/ISmartVaultYieldManager.sol b/contracts/interfaces/ISmartVaultYieldManager.sol index 800917d..1892e33 100644 --- a/contracts/interfaces/ISmartVaultYieldManager.sol +++ b/contracts/interfaces/ISmartVaultYieldManager.sol @@ -2,5 +2,5 @@ pragma solidity 0.8.17; interface ISmartVaultYieldManager { - function depositYield(bytes32 _symbol, uint256 _euroPercentage) external payable; + function depositYield(address _collateralToken, uint256 _euroPercentage) external payable returns (address vault0, address vault1); } \ No newline at end of file diff --git a/contracts/interfaces/ISwapRouter.sol b/contracts/interfaces/ISwapRouter.sol index cd6d2c1..bca6e1c 100644 --- a/contracts/interfaces/ISwapRouter.sol +++ b/contracts/interfaces/ISwapRouter.sol @@ -12,6 +12,39 @@ interface ISwapRouter { uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; } + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + } function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); + + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); } diff --git a/contracts/interfaces/IUniProxy.sol b/contracts/interfaces/IUniProxy.sol new file mode 100644 index 0000000..8ef6cf2 --- /dev/null +++ b/contracts/interfaces/IUniProxy.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +interface IUniProxy { + function getDepositAmount(address pos, address token, uint256 _deposit) external view returns (uint256 amountStart, uint256 amountEnd); + function deposit(uint256 deposit0, uint256 deposit1, address to, address pos, uint256[4] memory minIn) external returns (uint256 shares); +} \ No newline at end of file diff --git a/contracts/interfaces/IWETH.sol b/contracts/interfaces/IWETH.sol index 8db5bcb..b800463 100644 --- a/contracts/interfaces/IWETH.sol +++ b/contracts/interfaces/IWETH.sol @@ -4,5 +4,6 @@ pragma solidity 0.8.17; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IWETH is IERC20 { + function deposit() external payable; function withdraw(uint256) external; } \ No newline at end of file diff --git a/contracts/test_utils/GammaVaultMock.sol b/contracts/test_utils/GammaVaultMock.sol new file mode 100644 index 0000000..bf2401a --- /dev/null +++ b/contracts/test_utils/GammaVaultMock.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +// import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "contracts/interfaces/IHypervisor.sol"; + +contract GammaVaultMock is IHypervisor, ERC20 { + address public immutable token0; + address public immutable token1; + + constructor (string memory _name, string memory _symbol, address _token0, address _token1) ERC20(_name, _symbol) { + token0 = _token0; + token1 = _token1; + } + + function getTotalAmounts() public view returns (uint256 total0, uint256 total1) { + total0 = IERC20(token0).balanceOf(address(this)); + total1 = IERC20(token1).balanceOf(address(this)); + } + + function deposit( + uint256 deposit0, + uint256 deposit1, + address to, + address from, + uint256[4] memory inMin + ) external returns (uint256 shares) { + IERC20(token0).transferFrom(from, address(this), deposit0); + IERC20(token1).transferFrom(from, address(this), deposit1); + // simplified calculation because our mock will not deal with a changing swap rate + _mint(to, deposit0); + } +} \ No newline at end of file diff --git a/contracts/test_utils/MockSwapRouter.sol b/contracts/test_utils/MockSwapRouter.sol index 5864829..8c5e46c 100644 --- a/contracts/test_utils/MockSwapRouter.sol +++ b/contracts/test_utils/MockSwapRouter.sol @@ -37,4 +37,14 @@ contract MockSwapRouter is ISwapRouter { sqrtPriceLimitX96, txValue ); } + + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut) { + + } + + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn) { + + } + + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn) {} } \ No newline at end of file diff --git a/contracts/test_utils/MockWETH.sol b/contracts/test_utils/MockWETH.sol index e68ac40..212cf23 100644 --- a/contracts/test_utils/MockWETH.sol +++ b/contracts/test_utils/MockWETH.sol @@ -9,6 +9,13 @@ contract MockWETH is IWETH, ERC20 { constructor() ERC20("Wrapped Ether", "WETH") { } - function withdraw(uint256) external { + function withdraw(uint256 _value) external { + _burn(msg.sender, _value); + (bool sent, ) = payable(msg.sender).call{value: _value}(""); + require(sent); + } + + function deposit() external payable { + _mint(msg.sender, msg.value); } } \ No newline at end of file diff --git a/contracts/test_utils/UniProxyMock.sol b/contracts/test_utils/UniProxyMock.sol new file mode 100644 index 0000000..6490f50 --- /dev/null +++ b/contracts/test_utils/UniProxyMock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; + +import "contracts/interfaces/IHypervisor.sol"; +import "contracts/interfaces/IUniProxy.sol"; + +contract UniProxyMock is IUniProxy { + function getDepositAmount(address pos, address token, uint256 _deposit) external view returns (uint256 amountStart, uint256 amountEnd) { + } + + function deposit(uint256 deposit0, uint256 deposit1, address to, address pos, uint256[4] memory minIn) external returns (uint256 shares) { + IHypervisor(pos).deposit(deposit0, deposit1, to, to, minIn); + } +} \ No newline at end of file diff --git a/scripts/liquidate.js b/scripts/liquidate.js new file mode 100644 index 0000000..064af0a --- /dev/null +++ b/scripts/liquidate.js @@ -0,0 +1,13 @@ +const { ethers } = require("hardhat"); +// const abi = '[{"inputs":[{"internalType":"address","name":"_clearance","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"clearance","outputs":[{"internalType":"contract IClearing","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"deposit0","type":"uint256"},{"internalType":"uint256","name":"deposit1","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"pos","type":"address"},{"internalType":"uint256[4]","name":"minIn","type":"uint256[4]"}],"name":"deposit","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"pos","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"_deposit","type":"uint256"}],"name":"getDepositAmount","outputs":[{"internalType":"uint256","name":"amountStart","type":"uint256"},{"internalType":"uint256","name":"amountEnd","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newClearance","type":"address"}],"name":"transferClearance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]' +const abi = '[{"inputs":[{"internalType":"address","name":"_staking","type":"address"},{"internalType":"address","name":"_euros","type":"address"},{"internalType":"address","name":"_tokenManager","type":"address"},{"internalType":"address","name":"_smartVaultManager","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AccessControlBadConfirmation","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"neededRole","type":"bytes32"}],"name":"AccessControlUnauthorizedAccount","type":"error"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"name":"AddressEmptyCode","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"AddressInsufficientBalance","type":"error"},{"inputs":[],"name":"FailedInnerCall","type":"error"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"SafeERC20FailedOperation","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"airdropToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"dropFees","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tokenID","type":"uint256"}],"name":"liquidateVault","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"callerConfirmation","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}]' + +async function main() { + const manager = await ethers.getContractAt('SmartVaultManagerV5', '0xba169cceCCF7aC51dA223e04654Cf16ef41A68CC'); + console.log(await manager.estimateGas.vaultData(202)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/scripts/ratio.js b/scripts/ratio.js new file mode 100644 index 0000000..2643e38 --- /dev/null +++ b/scripts/ratio.js @@ -0,0 +1,59 @@ +const { ethers } = require("hardhat"); +const abi = '[{"inputs":[{"internalType":"address","name":"_clearance","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"clearance","outputs":[{"internalType":"contract IClearing","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"deposit0","type":"uint256"},{"internalType":"uint256","name":"deposit1","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"pos","type":"address"},{"internalType":"uint256[4]","name":"minIn","type":"uint256[4]"}],"name":"deposit","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"pos","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"_deposit","type":"uint256"}],"name":"getDepositAmount","outputs":[{"internalType":"uint256","name":"amountStart","type":"uint256"},{"internalType":"uint256","name":"amountEnd","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newClearance","type":"address"}],"name":"transferClearance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}]' + +async function main() { + const WETH = await ethers.getContractAt('IWETH', '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'); + const WBTC = await ethers.getContractAt('IERC20', '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f'); + const uniproxy = await ethers.getContractAt(JSON.parse(abi), '0x82FcEB07a4D01051519663f6c1c919aF21C27845'); + const router = await ethers.getContractAt('ISwapRouter', '0xE592427A0AEce92De3Edee1F18E0157C05861564'); + const hypervisor = await ethers.getContractAt('IERC20', '0x52ee1ffba696c5e9b0bc177a9f8a3098420ea691'); + const [ signer ] = await ethers.getSigners(); + + const amount = ethers.utils.parseEther('0.5'); + // await router.exactOutput({ + // path: ethers.utils.solidityPack(['address', 'uint24', 'address'], [WBTC.address, 500, WETH.address]), + // recipient: signer.address, + // deadline: Math.floor(new Date / 1000) + 60, + // amountOut: 2000000, + // amountInMaximum: amount + // }, {value: amount}); + + // await WETH.deposit({value: amount}) + + // let {amountStart, amountEnd} = await uniproxy.getDepositAmount(hypervisor, WETH.address, await WETH.balanceOf(signer.address)); + // let wbtcBalance = await WBTC.balanceOf(signer.address); + // let divver = 10; + // while (wbtcBalance.lt(amountStart)) { + // const toSwapOut = amountEnd.sub(wbtcBalance).div(divver); + // const maxSwapIn = await WETH.balanceOf(signer.address); + // await WETH.approve(router.address, maxSwapIn); + // await router.exactOutput({ + // path: ethers.utils.solidityPack(['address', 'uint24', 'address'], [WBTC.address, 500, WETH.address]), + // recipient: signer.address, + // deadline: Math.floor(new Date / 1000) + 60, + // amountOut: toSwapOut, + // amountInMaximum: maxSwapIn + // }); + + // ({amountStart, amountEnd} = await uniproxy.getDepositAmount(hypervisor, WETH.address, await WETH.balanceOf(signer.address))); + // wbtcBalance = await WBTC.balanceOf(signer.address); + // if (divver > 2) divver--; + // console.log('amountStart', amountStart); + // console.log('amountEnd', amountEnd); + // console.log('wbtcBalance', wbtcBalance); + // console.log('divver', divver) + // console.log('---') + // } + + // console.log(await WETH.balanceOf(signer.address)); + // console.log(await uniproxy.getDepositAmount(hypervisor, WETH.address, await WETH.balanceOf(signer.address))); + // console.log(await WBTC.balanceOf(signer.address)); + + + // --------- +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/test/SmartVault.js b/test/SmartVault.js index bbc7955..4ecb968 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -1,10 +1,10 @@ const { ethers } = require('hardhat'); const { BigNumber } = ethers; const { expect } = require('chai'); -const { DEFAULT_ETH_USD_PRICE, DEFAULT_EUR_USD_PRICE, DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, getCollateralOf, ETH, getNFTMetadataContract, fullyUpgradedSmartVaultManager, TEST_VAULT_LIMIT } = require('./common'); +const { DEFAULT_ETH_USD_PRICE, DEFAULT_EUR_USD_PRICE, DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, getCollateralOf, ETH, getNFTMetadataContract, fullyUpgradedSmartVaultManager, TEST_VAULT_LIMIT, WETH_ADDRESS } = require('./common'); const { HUNDRED_PC } = require('./common'); -let VaultManager, Vault, TokenManager, ClEthUsd, EUROs, MockSwapRouter, MockWeth, admin, user, otherUser, protocol; +let VaultManager, Vault, TokenManager, ClEthUsd, EUROs, MockSwapRouter, MockWeth, admin, user, otherUser, protocol, YieldManager; describe('SmartVault', async () => { beforeEach(async () => { @@ -20,7 +20,15 @@ describe('SmartVault', async () => { const NFTMetadataGenerator = await (await getNFTMetadataContract()).deploy(); MockSwapRouter = await (await ethers.getContractFactory('MockSwapRouter')).deploy(); MockWeth = await (await ethers.getContractFactory('MockWETH')).deploy(); - const YieldManager = await (await ethers.getContractFactory('SmartVaultYieldManager')).deploy(); + const EURA = await (await ethers.getContractFactory('ERC20Mock')).deploy('EURA', 'EURA', 18); + const UniProxyMock = await (await ethers.getContractFactory('UniProxyMock')).deploy(); + const EUROsGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( + 'EUROs-EURA', 'EUROs-EURA', EUROs.address, EURA.address + ); + YieldManager = await (await ethers.getContractFactory('SmartVaultYieldManager')).deploy( + EUROs.address, EURA.address, MockWeth.address, UniProxyMock.address, MockSwapRouter.address, EUROsGammaVaultMock.address, + MockSwapRouter.address + ); VaultManager = await fullyUpgradedSmartVaultManager( DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, EUROs.address, protocol.address, protocol.address, TokenManager.address, SmartVaultDeployer.address, @@ -440,6 +448,17 @@ describe('SmartVault', async () => { describe.only('yield', async () => { it('puts all of given collateral asset into yield', async () => { + + + const WETHGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( + 'WETH-WBTC', 'WETH-WBTC', WETH_ADDRESS, WBTC.address + ); + + await YieldManager.addVaultData( + WETH_ADDRESS, , 500, + ethers.utils.solidityPack(['address', 'uint24', 'address'], [WETH_ADDRESS, 3000, EURA.address]) + ) + const ethCollateral = ethers.utils.parseEther('0.1') await user.sendTransaction({ to: Vault.address, value: ethCollateral }); From 5e0656aefe8e8e516e6e5f23744e103b93ae43c1 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 14 Aug 2024 15:34:33 +0200 Subject: [PATCH 08/33] use custom errors in v4 vaults --- contracts/SmartVaultV4.sol | 49 ++++++++----- contracts/SmartVaultYieldManager.sol | 8 ++- contracts/test_utils/GammaVaultMock.sol | 2 + contracts/test_utils/MockSwapRouter.sol | 43 +++++++++-- contracts/test_utils/MockWETH.sol | 8 ++- contracts/test_utils/UniProxyMock.sol | 14 +++- test/SmartVault.js | 94 ++++++++++++++++++------- test/smartVaultManager.js | 2 +- 8 files changed, 166 insertions(+), 54 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index c2a43ec..734ea1a 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -13,11 +13,11 @@ import "contracts/interfaces/ISwapRouter.sol"; import "contracts/interfaces/ITokenManager.sol"; import "contracts/interfaces/IWETH.sol"; +import "hardhat/console.sol"; + contract SmartVaultV4 is ISmartVault { using SafeERC20 for IERC20; - string private constant INVALID_USER = "err-invalid-user"; - string private constant UNDER_COLL = "err-under-coll"; uint8 private constant version = 4; bytes32 private constant vaultType = bytes32("EUROs"); bytes32 private immutable NATIVE; @@ -35,6 +35,10 @@ contract SmartVaultV4 is ISmartVault { event EUROsMinted(address to, uint256 amount, uint256 fee); event EUROsBurned(uint256 amount, uint256 fee); + struct YieldPair { address token0; uint256 amount0; address token1; uint256 amount1; } + error InvalidUser(); + error InvalidRequest(); + constructor(bytes32 _native, address _manager, address _owner, address _euros, address _priceCalculator) { NATIVE = _native; owner = _owner; @@ -44,22 +48,22 @@ contract SmartVaultV4 is ISmartVault { } modifier onlyVaultManager { - require(msg.sender == manager, INVALID_USER); + if (msg.sender != manager) revert InvalidUser(); _; } modifier onlyOwner { - require(msg.sender == owner, INVALID_USER); + if (msg.sender != owner) revert InvalidUser(); _; } modifier ifMinted(uint256 _amount) { - require(minted >= _amount, "err-insuff-minted"); + if (minted < _amount) revert InvalidRequest(); _; } modifier ifNotLiquidated { - require(!liquidated, "err-liquidated"); + if (liquidated) revert InvalidRequest(); _; } @@ -85,7 +89,7 @@ contract SmartVaultV4 is ISmartVault { } else { // TODO how do we deal with WETH as underlying token? // add WETH as collateral? or check for it here? - for (uint256 j = 0; i < _acceptedTokens.length; j++) { + for (uint256 j = 0; j < _acceptedTokens.length; j++) { ITokenManager.Token memory _token = _acceptedTokens[j]; if (_token.addr == _token0) _euros += calculator.tokenToEur(_token, _underlying0); if (_token.addr == _token1) _euros += calculator.tokenToEur(_token, _underlying1); @@ -139,7 +143,7 @@ contract SmartVaultV4 is ISmartVault { function liquidateNative() private { if (address(this).balance != 0) { (bool sent,) = payable(ISmartVaultManagerV3(manager).protocol()).call{value: address(this).balance}(""); - require(sent, "err-native-liquidate"); + if (!sent) revert InvalidRequest(); } } @@ -148,7 +152,7 @@ contract SmartVaultV4 is ISmartVault { } function liquidate() external onlyVaultManager { - require(undercollateralised(), "err-not-liquidatable"); + if (!undercollateralised()) revert InvalidRequest(); liquidated = true; minted = 0; liquidateNative(); @@ -168,22 +172,22 @@ contract SmartVaultV4 is ISmartVault { } function removeCollateralNative(uint256 _amount, address payable _to) external onlyOwner { - require(canRemoveCollateral(getTokenManager().getToken(NATIVE), _amount), UNDER_COLL); + if (!canRemoveCollateral(getTokenManager().getToken(NATIVE), _amount)) revert InvalidRequest(); (bool sent,) = _to.call{value: _amount}(""); - require(sent, "err-native-call"); + if (!sent) revert InvalidRequest(); emit CollateralRemoved(NATIVE, _amount, _to); } function removeCollateral(bytes32 _symbol, uint256 _amount, address _to) external onlyOwner { ITokenManager.Token memory token = getTokenManager().getToken(_symbol); - require(canRemoveCollateral(token, _amount), UNDER_COLL); + if (!canRemoveCollateral(token, _amount)) revert InvalidRequest(); IERC20(token.addr).safeTransfer(_to, _amount); emit CollateralRemoved(_symbol, _amount, _to); } function removeAsset(address _tokenAddr, uint256 _amount, address _to) external onlyOwner { ITokenManager.Token memory token = getTokenManager().getTokenIfExists(_tokenAddr); - if (token.addr == _tokenAddr) require(canRemoveCollateral(token, _amount), UNDER_COLL); + if (token.addr == _tokenAddr && !canRemoveCollateral(token, _amount)) revert InvalidRequest(); IERC20(_tokenAddr).safeTransfer(_to, _amount); emit AssetRemoved(_tokenAddr, _amount, _to); } @@ -194,7 +198,7 @@ contract SmartVaultV4 is ISmartVault { function mint(address _to, uint256 _amount) external onlyOwner ifNotLiquidated { uint256 fee = _amount * ISmartVaultManagerV3(manager).mintFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC(); - require(fullyCollateralised(_amount + fee), UNDER_COLL); + if (!fullyCollateralised(_amount + fee)) revert InvalidRequest(); minted = minted + _amount + fee; EUROs.mint(_to, _amount); EUROs.mint(ISmartVaultManagerV3(manager).protocol(), fee); @@ -215,7 +219,7 @@ contract SmartVaultV4 is ISmartVault { for (uint256 i = 0; i < tokens.length; i++) { if (tokens[i].symbol == _symbol) _token = tokens[i]; } - require(_token.symbol != bytes32(0), "err-invalid-swap"); + if (_token.symbol == bytes32(0)) revert InvalidRequest(); } function getSwapAddressFor(bytes32 _symbol) private view returns (address) { @@ -225,7 +229,7 @@ contract SmartVaultV4 is ISmartVault { function executeNativeSwapAndFee(ISwapRouter.ExactInputSingleParams memory _params, uint256 _swapFee) private { (bool sent,) = payable(ISmartVaultManagerV3(manager).protocol()).call{value: _swapFee}(""); - require(sent, "err-swap-fee-native"); + if (!sent) revert InvalidRequest(); ISwapRouter(ISmartVaultManagerV3(manager).swapRouter2()).exactInputSingle{value: _params.amountIn}(_params); } @@ -277,6 +281,19 @@ contract SmartVaultV4 is ISmartVault { vaultTokens.push(_vault2); } + function yieldAssets() external view returns (YieldPair[] memory _yieldPairs) { + for (uint256 i = 0; i < vaultTokens.length; i++) { + IHypervisor _vaultToken = IHypervisor(vaultTokens[i]); + uint256 _balance = _vaultToken.balanceOf(address(this)); + uint256 _vaultTotal = _vaultToken.totalSupply(); + (uint256 _underlyingTotal0, uint256 _underlyingTotal1) = _vaultToken.getTotalAmounts(); + _yieldPairs[i].token0 = _vaultToken.token0(); + _yieldPairs[i].token1 = _vaultToken.token1(); + _yieldPairs[i].amount0 = _balance * _underlyingTotal0 / _vaultTotal; + _yieldPairs[i].amount1 = _balance * _underlyingTotal1 / _vaultTotal; + } + } + function setOwner(address _newOwner) external onlyVaultManager { owner = _newOwner; } diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index 62368d6..5693d76 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -7,6 +7,8 @@ import "contracts/interfaces/ISwapRouter.sol"; import "contracts/interfaces/IUniProxy.sol"; import "contracts/interfaces/IWETH.sol"; +import "hardhat/console.sol"; + contract SmartVaultYieldManager is ISmartVaultYieldManager { address private immutable EUROs; address private immutable EURA; @@ -117,8 +119,10 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { } function depositYield(address _collateralToken, uint256 _euroPercentage) external payable returns (address _vault0, address _vault1) { - if (_collateralToken == address(0)) _collateralToken = WETH; - IWETH(WETH).deposit{value: msg.value}(); + if (_collateralToken == address(0)) { + _collateralToken = WETH; + IWETH(WETH).deposit{value: msg.value}(); + } euroDeposit(_collateralToken, _euroPercentage); VaultData memory _vaultData = vaultData[_collateralToken]; otherDeposit(_collateralToken, _vaultData); diff --git a/contracts/test_utils/GammaVaultMock.sol b/contracts/test_utils/GammaVaultMock.sol index bf2401a..24d88b3 100644 --- a/contracts/test_utils/GammaVaultMock.sol +++ b/contracts/test_utils/GammaVaultMock.sol @@ -5,6 +5,8 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "contracts/interfaces/IHypervisor.sol"; +import "hardhat/console.sol"; + contract GammaVaultMock is IHypervisor, ERC20 { address public immutable token0; address public immutable token1; diff --git a/contracts/test_utils/MockSwapRouter.sol b/contracts/test_utils/MockSwapRouter.sol index 8c5e46c..775c47b 100644 --- a/contracts/test_utils/MockSwapRouter.sol +++ b/contracts/test_utils/MockSwapRouter.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "contracts/interfaces/ISwapRouter.sol"; contract MockSwapRouter is ISwapRouter { @@ -14,12 +15,14 @@ contract MockSwapRouter is ISwapRouter { uint160 private sqrtPriceLimitX96; uint256 private txValue; + mapping(address => mapping(address => uint256)) private rates; + struct MockSwapData { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 deadline; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; uint256 txValue; } - function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut) { + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 _amountOut) { tokenIn = params.tokenIn; tokenOut = params.tokenOut; fee = params.fee; @@ -29,6 +32,13 @@ contract MockSwapRouter is ISwapRouter { amountOutMinimum = params.amountOutMinimum; sqrtPriceLimitX96 = params.sqrtPriceLimitX96; txValue = msg.value; + + _amountOut = rates[tokenIn][tokenOut] * amountIn / 1e18; + require(_amountOut > amountOutMinimum); + if (msg.value == 0) { + IERC20(tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + } + IERC20(tokenOut).transfer(recipient, _amountOut); } function receivedSwap() external view returns (MockSwapData memory) { @@ -38,13 +48,36 @@ contract MockSwapRouter is ISwapRouter { ); } - function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut) { - + function exactInput(ExactInputParams calldata params) external payable returns (uint256 _amountOut) { + (address _tokenIn,, address _tokenOut) = abi.decode(params.path, (address, uint24, address)); + _amountOut = rates[_tokenIn][_tokenOut] * params.amountIn / 1e18; + require(_amountOut > params.amountOutMinimum); + if (msg.value == 0) { + IERC20(_tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + } + IERC20(_tokenOut).transfer(params.recipient, _amountOut); } - function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn) { + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 _amountIn) { + (address _tokenOut, , address _tokenIn) = abi.decode(params.path, (address, uint24, address)); + _amountIn = params.amountOut * 1e18 / rates[_tokenIn][_tokenOut]; + require(_amountIn < params.amountInMaximum); + if (msg.value == 0) { + IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn); + } + IERC20(_tokenOut).transfer(params.recipient, params.amountOut); + } + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 _amountIn) { + _amountIn = params.amountOut * 1e18 / rates[params.tokenIn][params.tokenOut]; + require(_amountIn < params.amountInMaximum); + if (msg.value == 0) { + IERC20(params.tokenIn).transferFrom(msg.sender, address(this), _amountIn); + } + IERC20(params.tokenOut).transfer(params.recipient, params.amountOut); } - function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn) {} + function setRate(address _tokenIn, address _tokenOut, uint256 _rate) external { + rates[_tokenIn][_tokenOut] = _rate; + } } \ No newline at end of file diff --git a/contracts/test_utils/MockWETH.sol b/contracts/test_utils/MockWETH.sol index 212cf23..6721d73 100644 --- a/contracts/test_utils/MockWETH.sol +++ b/contracts/test_utils/MockWETH.sol @@ -1,14 +1,16 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "contracts/test_utils/ERC20Mock.sol"; import "contracts/interfaces/IWETH.sol"; -contract MockWETH is IWETH, ERC20 { +contract MockWETH is IWETH, ERC20Mock { - constructor() ERC20("Wrapped Ether", "WETH") { + constructor() ERC20Mock("Wrapped Ether", "WETH", 18) { } + receive() external payable {} + function withdraw(uint256 _value) external { _burn(msg.sender, _value); (bool sent, ) = payable(msg.sender).call{value: _value}(""); diff --git a/contracts/test_utils/UniProxyMock.sol b/contracts/test_utils/UniProxyMock.sol index 6490f50..ad17b71 100644 --- a/contracts/test_utils/UniProxyMock.sol +++ b/contracts/test_utils/UniProxyMock.sol @@ -5,10 +5,18 @@ import "contracts/interfaces/IHypervisor.sol"; import "contracts/interfaces/IUniProxy.sol"; contract UniProxyMock is IUniProxy { - function getDepositAmount(address pos, address token, uint256 _deposit) external view returns (uint256 amountStart, uint256 amountEnd) { + mapping(address => uint256) private ratios; + + function getDepositAmount(address vault, address token, uint256 _deposit) external view returns (uint256 amountStart, uint256 amountEnd) { + uint256 _mid = ratios[vault] * _deposit / 1e18; + return (_mid * 999 / 1000, _mid * 1001 / 1000); + } + + function deposit(uint256 deposit0, uint256 deposit1, address to, address vault, uint256[4] memory minIn) external returns (uint256 shares) { + IHypervisor(vault).deposit(deposit0, deposit1, to, msg.sender, minIn); } - function deposit(uint256 deposit0, uint256 deposit1, address to, address pos, uint256[4] memory minIn) external returns (uint256 shares) { - IHypervisor(pos).deposit(deposit0, deposit1, to, to, minIn); + function setRatio(address _vault, uint256 _ratio) external { + ratios[_vault] = _ratio; } } \ No newline at end of file diff --git a/test/SmartVault.js b/test/SmartVault.js index 4ecb968..75b1c1b 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -1,10 +1,10 @@ const { ethers } = require('hardhat'); const { BigNumber } = ethers; const { expect } = require('chai'); -const { DEFAULT_ETH_USD_PRICE, DEFAULT_EUR_USD_PRICE, DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, getCollateralOf, ETH, getNFTMetadataContract, fullyUpgradedSmartVaultManager, TEST_VAULT_LIMIT, WETH_ADDRESS } = require('./common'); +const { DEFAULT_ETH_USD_PRICE, DEFAULT_EUR_USD_PRICE, DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, getCollateralOf, ETH, getNFTMetadataContract, fullyUpgradedSmartVaultManager, TEST_VAULT_LIMIT } = require('./common'); const { HUNDRED_PC } = require('./common'); -let VaultManager, Vault, TokenManager, ClEthUsd, EUROs, MockSwapRouter, MockWeth, admin, user, otherUser, protocol, YieldManager; +let VaultManager, Vault, TokenManager, ClEthUsd, EUROs, EURA, MockSwapRouter, MockWeth, admin, user, otherUser, protocol, YieldManager, UniProxyMock, EUROsGammaVaultMock; describe('SmartVault', async () => { beforeEach(async () => { @@ -20,9 +20,9 @@ describe('SmartVault', async () => { const NFTMetadataGenerator = await (await getNFTMetadataContract()).deploy(); MockSwapRouter = await (await ethers.getContractFactory('MockSwapRouter')).deploy(); MockWeth = await (await ethers.getContractFactory('MockWETH')).deploy(); - const EURA = await (await ethers.getContractFactory('ERC20Mock')).deploy('EURA', 'EURA', 18); - const UniProxyMock = await (await ethers.getContractFactory('UniProxyMock')).deploy(); - const EUROsGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( + EURA = await (await ethers.getContractFactory('ERC20Mock')).deploy('EURA', 'EURA', 18); + UniProxyMock = await (await ethers.getContractFactory('UniProxyMock')).deploy(); + EUROsGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( 'EUROs-EURA', 'EUROs-EURA', EUROs.address, EURA.address ); YieldManager = await (await ethers.getContractFactory('SmartVaultYieldManager')).deploy( @@ -37,6 +37,7 @@ describe('SmartVault', async () => { ); await SmartVaultIndex.setVaultManager(VaultManager.address); await EUROs.grantRole(await EUROs.DEFAULT_ADMIN_ROLE(), VaultManager.address); + await EUROs.grantRole(await EUROs.MINTER_ROLE(), admin.address); await VaultManager.connect(user).mint(); const [ vaultID ] = await VaultManager.vaultIDs(user.address); const { status } = await VaultManager.vaultData(vaultID); @@ -47,7 +48,7 @@ describe('SmartVault', async () => { describe('ownership', async () => { it('will not allow setting of new owner if not manager', async () => { const ownerUpdate = Vault.connect(user).setOwner(otherUser.address); - await expect(ownerUpdate).to.be.revertedWith('err-invalid-user'); + await expect(ownerUpdate).to.be.revertedWithCustomError(Vault, 'InvalidUser'); }); }); @@ -122,7 +123,7 @@ describe('SmartVault', async () => { expect(getCollateralOf('ETH', collateral).amount).to.equal(value); let remove = Vault.connect(otherUser).removeCollateralNative(value, user.address); - await expect(remove).to.be.revertedWith('err-invalid-user'); + await expect(remove).to.be.revertedWithCustomError(Vault, 'InvalidUser'); remove = Vault.connect(user).removeCollateralNative(half, user.address); await expect(remove).not.to.be.reverted; @@ -136,7 +137,7 @@ describe('SmartVault', async () => { // cannot remove any eth remove = Vault.connect(user).removeCollateralNative(ethers.utils.parseEther('0.0001'), user.address); - await expect(remove).to.be.revertedWith('err-under-coll'); + await expect(remove).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); }); it('allows removal of ERC20 if owner and it will not undercollateralise vault', async () => { @@ -156,7 +157,7 @@ describe('SmartVault', async () => { expect(getCollateralOf('USDT', collateral).amount).to.equal(value); let remove = Vault.connect(otherUser).removeCollateral(USDTBytes, value, user.address); - await expect(remove).to.be.revertedWith('err-invalid-user'); + await expect(remove).to.be.revertedWithCustomError(Vault, 'InvalidUser'); remove = Vault.connect(user).removeCollateral(USDTBytes, half, user.address); await expect(remove).not.to.be.reverted; @@ -170,7 +171,7 @@ describe('SmartVault', async () => { // cannot remove any eth remove = Vault.connect(user).removeCollateral(ethers.utils.formatBytes32String('USDT'), 1000000, user.address); - await expect(remove).to.be.revertedWith('err-under-coll'); + await expect(remove).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); }); it('allows removal of ERC20s that are or are not valid collateral, if not undercollateralising', async () => { @@ -193,13 +194,13 @@ describe('SmartVault', async () => { await Vault.connect(user).mint(user.address, maxMintable.div(2)); - await expect(Vault.removeAsset(SUSD6.address, SUSD6value, user.address)).to.be.revertedWith('err-invalid-user'); + await expect(Vault.removeAsset(SUSD6.address, SUSD6value, user.address)).to.be.revertedWithCustomError(Vault, 'InvalidUser'); await Vault.connect(user).removeAsset(SUSD6.address, SUSD6value, user.address); expect(await SUSD6.balanceOf(Vault.address)).to.equal(0); expect(await SUSD6.balanceOf(user.address)).to.equal(SUSD6value); - await expect(Vault.connect(user).removeAsset(SUSD18.address, SUSD18value, user.address)).to.be.revertedWith('err-under-coll'); + await expect(Vault.connect(user).removeAsset(SUSD18.address, SUSD18value, user.address)).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); // partial removal, because some needed as collateral const part = SUSD18value.div(3); @@ -214,13 +215,13 @@ describe('SmartVault', async () => { describe('minting', async () => { it('only allows the vault owner to mint from smart vault directly', async () => { const mintedValue = ethers.utils.parseEther('100'); - await expect(Vault.connect(user).mint(user.address, mintedValue)).to.be.revertedWith('err-under-coll'); + await expect(Vault.connect(user).mint(user.address, mintedValue)).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); const collateral = ethers.utils.parseEther('1'); await user.sendTransaction({to: Vault.address, value: collateral}); let mint = Vault.connect(otherUser).mint(user.address, mintedValue); - await expect(mint).to.be.revertedWith('err-invalid-user'); + await expect(mint).to.be.revertedWithCustomError(Vault, 'InvalidUser'); mint = Vault.connect(user).mint(user.address, mintedValue); await expect(mint).not.to.be.reverted; @@ -241,7 +242,7 @@ describe('SmartVault', async () => { const burnedValue = ethers.utils.parseEther('50'); let burn = Vault.connect(user).burn(burnedValue); - await expect(burn).to.be.revertedWith('err-insuff-minted'); + await expect(burn).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); // 100 to user // 1 to protocol @@ -292,12 +293,12 @@ describe('SmartVault', async () => { const mintedValue = ethers.utils.parseEther('900'); await Vault.connect(user).mint(user.address, mintedValue); - await expect(VaultManager.connect(protocol).liquidateVault(1)).to.be.revertedWith('vault-not-undercollateralised'); + await expect(VaultManager.connect(protocol).liquidateVault(1)).to.be.revertedWith('vault-not-undercollateralised') // drop price, now vault is liquidatable await ClEthUsd.setPrice(100000000000); - await expect(Vault.liquidate()).to.be.revertedWith('err-invalid-user'); + await expect(Vault.liquidate()).to.be.revertedWithCustomError(Vault, 'InvalidUser'); await expect(VaultManager.connect(protocol).liquidateVault(1)).not.to.be.reverted; const { minted, maxMintable, totalCollateralValue, collateral, liquidated } = await Vault.status(); @@ -323,7 +324,7 @@ describe('SmartVault', async () => { expect(liquidated).to.equal(true); await user.sendTransaction({to: Vault.address, value: ethValue.mul(2)}); - await expect(Vault.connect(user).mint(user.address, mintedValue)).to.be.revertedWith('err-liquidated'); + await expect(Vault.connect(user).mint(user.address, mintedValue)).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); }); }); @@ -344,7 +345,7 @@ describe('SmartVault', async () => { const swapValue = ethers.utils.parseEther('0.5'); const swap = Vault.connect(admin).swap(inToken, outToken, swapValue, 0); - await expect(swap).to.be.revertedWith('err-invalid-user'); + await expect(swap).to.be.revertedWithCustomError(Vault, 'InvalidUser'); }); it('invokes swaprouter with value for eth swap, paying fees to protocol', async () => { @@ -360,6 +361,12 @@ describe('SmartVault', async () => { const swapFee = swapValue.mul(PROTOCOL_FEE_RATE).div(HUNDRED_PC); const protocolBalance = await protocol.getBalance(); + + // load up mock swap router + await Stablecoin.mint(MockSwapRouter.address, 1_000_000_000_000); + // rate of eth / usd is default rate, scaled down from 8 dec (chainlink) to 6 dec (stablecoin decimals) + await MockSwapRouter.setRate(MockWeth.address, Stablecoin.address, DEFAULT_ETH_USD_PRICE / 100); + const swap = await Vault.connect(user).swap(inToken, outToken, swapValue, 0); const ts = (await ethers.provider.getBlock(swap.blockNumber)).timestamp; @@ -399,6 +406,12 @@ describe('SmartVault', async () => { // even if swap returned 0 assets, vault would remain above €600 required collateral value // minimum swap therefore 0 const protocolBalance = await protocol.getBalance(); + + // load up mock swap router + await Stablecoin.mint(MockSwapRouter.address, 1_000_000_000_000); + // rate of eth / usd is default rate, scaled down from 8 dec (chainlink) to 6 dec (stablecoin decimals) + await MockSwapRouter.setRate(MockWeth.address, Stablecoin.address, DEFAULT_ETH_USD_PRICE / 100); + const swap = await Vault.connect(user).swap(inToken, outToken, swapValue, swapMinimum); const ts = (await ethers.provider.getBlock(swap.blockNumber)).timestamp; @@ -426,6 +439,14 @@ describe('SmartVault', async () => { const swapValue = ethers.utils.parseEther('50'); const swapFee = swapValue.mul(PROTOCOL_FEE_RATE).div(HUNDRED_PC); const actualSwap = swapValue.sub(swapFee); + + // load up mock weth + await admin.sendTransaction({ to: MockWeth.address, value: ethers.utils.parseEther('1') }); + // load up mock swap router + await MockWeth.mint(MockSwapRouter.address, ethers.utils.parseEther('1')); + // rate of usd / eth is 1 / DEFAULT RATE * 10 ^ 20 (to scale from 6 dec to 18, and remove 8 dec scale down from chainlink price) + await MockSwapRouter.setRate(Stablecoin.address, MockWeth.address, BigNumber.from(10).pow(20).div(DEFAULT_ETH_USD_PRICE)); + const swap = await Vault.connect(user).swap(inToken, outToken, swapValue, 0); const ts = (await ethers.provider.getBlock(swap.blockNumber)).timestamp; @@ -446,19 +467,42 @@ describe('SmartVault', async () => { }); }); - describe.only('yield', async () => { + describe('yield', async () => { it('puts all of given collateral asset into yield', async () => { + const WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); + const CL_WBTC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC / USD'); + await CL_WBTC_USD.setPrice(DEFAULT_ETH_USD_PRICE.mul(20)); + await TokenManager.addAcceptedToken(WBTC.address, CL_WBTC_USD.address); + await TokenManager.addAcceptedToken(MockWeth.address, ClEthUsd.address); - + // fake gamma vault for WETH + WBTC const WETHGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( - 'WETH-WBTC', 'WETH-WBTC', WETH_ADDRESS, WBTC.address + 'WETH-WBTC', 'WETH-WBTC', MockWeth.address, WBTC.address ); + // data about how yield manager converts collateral to EURA, vault addresses etc await YieldManager.addVaultData( - WETH_ADDRESS, , 500, - ethers.utils.solidityPack(['address', 'uint24', 'address'], [WETH_ADDRESS, 3000, EURA.address]) + MockWeth.address, WETHGammaVaultMock.address, 500, + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [MockWeth.address, 3000, EURA.address]) ) + // ratio of euros vault is 1:1 + await UniProxyMock.setRatio(EUROsGammaVaultMock.address, ethers.utils.parseEther('1')); + // ratio of weth / wbtc vault is 1:1 in value, or 20:1 in unscaled numbers (20*10**10:1) in scaled + await UniProxyMock.setRatio(WETHGammaVaultMock.address, 5000000); + + // set fake rate for swap router: this is ETH / EUROs: ~1500 + await MockSwapRouter.setRate(MockWeth.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(ethers.utils.parseEther('1')).div(DEFAULT_EUR_USD_PRICE)) + // set fake rate for EURA / EURO: 1:1 + await MockSwapRouter.setRate(EURA.address, EUROs.address, ethers.utils.parseEther('1')); + // set fake rate for ETH / WBTC: 0.05 WBTC scaled down to 8 dec + await MockSwapRouter.setRate(MockWeth.address, WBTC.address, 5000000); + + // load up mock swap router + await EURA.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); + await EUROs.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); + await WBTC.mint(MockSwapRouter.address, ethers.utils.parseUnits('10', 8)); + const ethCollateral = ethers.utils.parseEther('0.1') await user.sendTransaction({ to: Vault.address, value: ethCollateral }); @@ -470,6 +514,8 @@ describe('SmartVault', async () => { ({ collateral, totalCollateralValue } = await Vault.status()); expect(getCollateralOf('ETH', collateral).amount).to.equal(0); expect(totalCollateralValue).to.eq(preYieldCollateral); + const yieldAssets = await Vault.yieldAssets(); + console.log(yieldAssets); }); }); }); \ No newline at end of file diff --git a/test/smartVaultManager.js b/test/smartVaultManager.js index 4318036..3cd34ba 100644 --- a/test/smartVaultManager.js +++ b/test/smartVaultManager.js @@ -27,7 +27,7 @@ describe('SmartVaultManager', async () => { DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, EUROs.address, protocol.address, liquidator.address, TokenManager.address, SmartVaultDeployer.address, SmartVaultIndex.address, NFTMetadataGenerator.address, WETH_ADDRESS, - MockSwapRouter.address, TEST_VAULT_LIMIT + MockSwapRouter.address, TEST_VAULT_LIMIT, ethers.constants.AddressZero ); await SmartVaultIndex.setVaultManager(VaultManager.address); await EUROs.grantRole(await EUROs.DEFAULT_ADMIN_ROLE(), VaultManager.address); From 1af0353a0e3db4378a2f14bed495433168829b1b Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 14 Aug 2024 17:15:22 +0200 Subject: [PATCH 09/33] add function to show underlying collateral values for yield --- contracts/SmartVaultV4.sol | 3 +++ test/SmartVault.js | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index 734ea1a..0ccb383 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -277,16 +277,19 @@ contract SmartVaultV4 is ISmartVault { ITokenManager.Token memory _token = getTokenManager().getToken(_symbol); uint256 _balance = getAssetBalance(_symbol, _token.addr); (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield{value: address(this).balance}(_token.addr, _euroPercentage); + // TODO make sure this is unique added vaultTokens.push(_vault1); vaultTokens.push(_vault2); } function yieldAssets() external view returns (YieldPair[] memory _yieldPairs) { + _yieldPairs = new YieldPair[](vaultTokens.length); for (uint256 i = 0; i < vaultTokens.length; i++) { IHypervisor _vaultToken = IHypervisor(vaultTokens[i]); uint256 _balance = _vaultToken.balanceOf(address(this)); uint256 _vaultTotal = _vaultToken.totalSupply(); (uint256 _underlyingTotal0, uint256 _underlyingTotal1) = _vaultToken.getTotalAmounts(); + _yieldPairs[i].token0 = _vaultToken.token0(); _yieldPairs[i].token1 = _vaultToken.token1(); _yieldPairs[i].amount0 = _balance * _underlyingTotal0 / _vaultTotal; diff --git a/test/SmartVault.js b/test/SmartVault.js index 75b1c1b..f23af66 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -468,6 +468,10 @@ describe('SmartVault', async () => { }); describe('yield', async () => { + it('fetches empty yield list', async () => { + + }); + it('puts all of given collateral asset into yield', async () => { const WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); const CL_WBTC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC / USD'); @@ -496,7 +500,8 @@ describe('SmartVault', async () => { // set fake rate for EURA / EURO: 1:1 await MockSwapRouter.setRate(EURA.address, EUROs.address, ethers.utils.parseEther('1')); // set fake rate for ETH / WBTC: 0.05 WBTC scaled down to 8 dec - await MockSwapRouter.setRate(MockWeth.address, WBTC.address, 5000000); + const WBTCPerETH = 5000000 + await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH); // load up mock swap router await EURA.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); @@ -513,9 +518,20 @@ describe('SmartVault', async () => { await Vault.depositYield(ETH, HUNDRED_PC.div(2)); ({ collateral, totalCollateralValue } = await Vault.status()); expect(getCollateralOf('ETH', collateral).amount).to.equal(0); - expect(totalCollateralValue).to.eq(preYieldCollateral); + // allow a delta of 2 wei in pre and post yield collateral, due to dividing etc + expect(totalCollateralValue).to.be.closeTo(preYieldCollateral, 2); + const yieldAssets = await Vault.yieldAssets(); - console.log(yieldAssets); + expect(yieldAssets).to.have.length(2); + expect([EUROs.address, EURA.address]).to.include(yieldAssets[0].token0); + expect([EUROs.address, EURA.address]).to.include(yieldAssets[0].token1); + expect(yieldAssets[0].amount0).to.be.closeTo(preYieldCollateral.div(4), 1); + expect(yieldAssets[0].amount1).to.be.closeTo(preYieldCollateral.div(4), 1); + expect([WBTC.address, MockWeth.address]).to.include(yieldAssets[1].token0); + expect([WBTC.address, MockWeth.address]).to.include(yieldAssets[1].token1); + expect(yieldAssets[1].amount0).to.equal(ethCollateral.div(4), 1); + // 0.1 ETH, quarter of which should be wbtc + expect(yieldAssets[1].amount1).to.be.closeTo(WBTCPerETH / 40, 1); }); }); }); \ No newline at end of file From 7a8d824356be5727fa3b8bb095182aa23ea4d5a5 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Thu, 15 Aug 2024 19:06:48 +0200 Subject: [PATCH 10/33] deposits yield for both eth + erc20s, of differing decimal accuracies --- contracts/SmartVaultV4.sol | 35 +++++++++++++-------- contracts/SmartVaultYieldManager.sol | 23 ++++++++------ contracts/test_utils/MockSwapRouter.sol | 2 ++ contracts/test_utils/UniProxyMock.sol | 8 ++--- contracts/versions/SmartVaultManagerV4.sol | 7 ++--- hardhat.config.js | 11 ++++++- test/SmartVault.js | 36 ++++++++++++++++++---- 7 files changed, 85 insertions(+), 37 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index 0ccb383..860b17f 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -104,7 +104,7 @@ contract SmartVaultV4 is ISmartVault { ITokenManager.Token[] memory acceptedTokens = tokenManager.getAcceptedTokens(); for (uint256 i = 0; i < acceptedTokens.length; i++) { ITokenManager.Token memory _token = acceptedTokens[i]; - euros += calculator.tokenToEur(_token, getAssetBalance(_token.symbol, _token.addr)); + euros += calculator.tokenToEur(_token, getAssetBalance(_token.addr)); } euros += yieldVaultCollateral(acceptedTokens); @@ -114,8 +114,8 @@ contract SmartVaultV4 is ISmartVault { return _collateral * ISmartVaultManagerV3(manager).HUNDRED_PC() / ISmartVaultManagerV3(manager).collateralRate(); } - function getAssetBalance(bytes32 _symbol, address _tokenAddress) private view returns (uint256 amount) { - return _symbol == NATIVE ? address(this).balance : IERC20(_tokenAddress).balanceOf(address(this)); + function getAssetBalance(address _tokenAddress) private view returns (uint256 amount) { + return _tokenAddress == address(0) ? address(this).balance : IERC20(_tokenAddress).balanceOf(address(this)); } function getAssets() private view returns (Asset[] memory) { @@ -124,7 +124,7 @@ contract SmartVaultV4 is ISmartVault { Asset[] memory assets = new Asset[](acceptedTokens.length); for (uint256 i = 0; i < acceptedTokens.length; i++) { ITokenManager.Token memory token = acceptedTokens[i]; - uint256 assetBalance = getAssetBalance(token.symbol, token.addr); + uint256 assetBalance = getAssetBalance(token.addr); assets[i] = Asset(token, assetBalance, calculator.tokenToEur(token, assetBalance)); } return assets; @@ -222,7 +222,7 @@ contract SmartVaultV4 is ISmartVault { if (_token.symbol == bytes32(0)) revert InvalidRequest(); } - function getSwapAddressFor(bytes32 _symbol) private view returns (address) { + function getTokenisedAddr(bytes32 _symbol) private view returns (address) { ITokenManager.Token memory _token = getToken(_symbol); return _token.addr == address(0) ? ISmartVaultManagerV3(manager).weth() : _token.addr; } @@ -255,12 +255,12 @@ contract SmartVaultV4 is ISmartVault { function swap(bytes32 _inToken, bytes32 _outToken, uint256 _amount, uint256 _requestedMinOut) external onlyOwner { uint256 swapFee = _amount * ISmartVaultManagerV3(manager).swapFeeRate() / ISmartVaultManagerV3(manager).HUNDRED_PC(); - address inToken = getSwapAddressFor(_inToken); + address inToken = getTokenisedAddr(_inToken); uint256 minimumAmountOut = calculateMinimumAmountOut(_inToken, _outToken, _amount + swapFee); if (_requestedMinOut > minimumAmountOut) minimumAmountOut = _requestedMinOut; ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: inToken, - tokenOut: getSwapAddressFor(_outToken), + tokenOut: getTokenisedAddr(_outToken), fee: 3000, recipient: address(this), deadline: block.timestamp + 60, @@ -273,13 +273,22 @@ contract SmartVaultV4 is ISmartVault { executeERC20SwapAndFee(params, swapFee); } + function addUniqueVaultToken(address _vault) private { + for (uint256 i = 0; i < vaultTokens.length; i++) { + if (vaultTokens[i] == _vault) return; + } + vaultTokens.push(_vault); + } + function depositYield(bytes32 _symbol, uint256 _euroPercentage) external { - ITokenManager.Token memory _token = getTokenManager().getToken(_symbol); - uint256 _balance = getAssetBalance(_symbol, _token.addr); - (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield{value: address(this).balance}(_token.addr, _euroPercentage); - // TODO make sure this is unique added - vaultTokens.push(_vault1); - vaultTokens.push(_vault2); + if (_symbol == NATIVE) IWETH(ISmartVaultManagerV3(manager).weth()).deposit{value: address(this).balance}(); + address _token = getTokenisedAddr(_symbol); + uint256 _balance = getAssetBalance(_token); + if (_balance == 0) revert InvalidRequest(); + IERC20(_token).safeApprove(ISmartVaultManagerV3(manager).yieldManager(), _balance); + (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield(_token, _euroPercentage); + addUniqueVaultToken(_vault1); + addUniqueVaultToken(_vault2); } function yieldAssets() external view returns (YieldPair[] memory _yieldPairs) { diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index 5693d76..d90b078 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "contracts/interfaces/IHypervisor.sol"; import "contracts/interfaces/ISmartVaultYieldManager.sol"; import "contracts/interfaces/ISwapRouter.sol"; @@ -10,6 +11,8 @@ import "contracts/interfaces/IWETH.sol"; import "hardhat/console.sol"; contract SmartVaultYieldManager is ISmartVaultYieldManager { + using SafeERC20 for IERC20; + address private immutable EUROs; address private immutable EURA; address private immutable WETH; @@ -50,7 +53,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { _divisor++; _tokenBTooLarge = false; } - IERC20(_tokenA).approve(_swapRouter, balance(_tokenA)); + IERC20(_tokenA).safeApprove(_swapRouter, balance(_tokenA)); try ISwapRouter(_swapRouter).exactOutputSingle(ISwapRouter.ExactOutputSingleParams({ tokenIn: _tokenA, tokenOut: _tokenB, @@ -63,12 +66,13 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { })) returns (uint256) {} catch { _divisor++; } + IERC20(_tokenA).safeApprove(_swapRouter, 0); } else { if (!_tokenBTooLarge) { _divisor++; _tokenBTooLarge = true; } - IERC20(_tokenB).approve(_swapRouter, (_tokenBBalance - _midRatio) / _divisor); + IERC20(_tokenB).safeApprove(_swapRouter, (_tokenBBalance - _midRatio) / _divisor); try ISwapRouter(_swapRouter).exactInputSingle(ISwapRouter.ExactInputSingleParams({ tokenIn: _tokenB, tokenOut: _tokenA, @@ -81,6 +85,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { })) returns (uint256) {} catch { _divisor++; } + IERC20(_tokenB).safeApprove(_swapRouter, 0); } _tokenBBalance = balance(_tokenB); (amountStart, amountEnd) = IUniProxy(uniProxy).getDepositAmount(_hypervisor, _tokenA, balance(_tokenA)); @@ -89,7 +94,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { function swapToEURA(address _collateralToken, uint256 _euroPercentage) private { uint256 _euroYieldPortion = balance(_collateralToken) * _euroPercentage / HUNDRED_PC; - IERC20(_collateralToken).approve(eurosRouter, _euroYieldPortion); + IERC20(_collateralToken).safeApprove(eurosRouter, _euroYieldPortion); ISwapRouter(eurosRouter).exactInput(ISwapRouter.ExactInputParams({ path: vaultData[_collateralToken].pathToEURA, recipient: address(this), @@ -97,14 +102,17 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { amountIn: _euroYieldPortion, amountOutMinimum: 1 })); + IERC20(_collateralToken).safeApprove(eurosRouter, 0); } function deposit(address _vault) private { address _token0 = IHypervisor(_vault).token0(); address _token1 = IHypervisor(_vault).token1(); - IERC20(_token0).approve(_vault, balance(_token0)); - IERC20(_token1).approve(_vault, balance(_token1)); + IERC20(_token0).safeApprove(_vault, balance(_token0)); + IERC20(_token1).safeApprove(_vault, balance(_token1)); IUniProxy(uniProxy).deposit(balance(_token0), balance(_token1), msg.sender, _vault, [uint256(0),uint256(0),uint256(0),uint256(0)]); + IERC20(_token0).safeApprove(_vault, 0); + IERC20(_token1).safeApprove(_vault, 0); } function euroDeposit(address _collateralToken, uint256 _euroPercentage) private { @@ -119,10 +127,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { } function depositYield(address _collateralToken, uint256 _euroPercentage) external payable returns (address _vault0, address _vault1) { - if (_collateralToken == address(0)) { - _collateralToken = WETH; - IWETH(WETH).deposit{value: msg.value}(); - } + IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), IERC20(_collateralToken).balanceOf(address(msg.sender))); euroDeposit(_collateralToken, _euroPercentage); VaultData memory _vaultData = vaultData[_collateralToken]; otherDeposit(_collateralToken, _vaultData); diff --git a/contracts/test_utils/MockSwapRouter.sol b/contracts/test_utils/MockSwapRouter.sol index 775c47b..d2dcc0e 100644 --- a/contracts/test_utils/MockSwapRouter.sol +++ b/contracts/test_utils/MockSwapRouter.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.17; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "contracts/interfaces/ISwapRouter.sol"; +import "hardhat/console.sol"; + contract MockSwapRouter is ISwapRouter { address private tokenIn; address private tokenOut; diff --git a/contracts/test_utils/UniProxyMock.sol b/contracts/test_utils/UniProxyMock.sol index ad17b71..9664731 100644 --- a/contracts/test_utils/UniProxyMock.sol +++ b/contracts/test_utils/UniProxyMock.sol @@ -5,10 +5,10 @@ import "contracts/interfaces/IHypervisor.sol"; import "contracts/interfaces/IUniProxy.sol"; contract UniProxyMock is IUniProxy { - mapping(address => uint256) private ratios; + mapping(address => mapping(address => uint256)) private ratios; function getDepositAmount(address vault, address token, uint256 _deposit) external view returns (uint256 amountStart, uint256 amountEnd) { - uint256 _mid = ratios[vault] * _deposit / 1e18; + uint256 _mid = ratios[vault][token] * _deposit / 1e18; return (_mid * 999 / 1000, _mid * 1001 / 1000); } @@ -16,7 +16,7 @@ contract UniProxyMock is IUniProxy { IHypervisor(vault).deposit(deposit0, deposit1, to, msg.sender, minIn); } - function setRatio(address _vault, uint256 _ratio) external { - ratios[_vault] = _ratio; + function setRatio(address _vault, address _inToken, uint256 _ratio) external { + ratios[_vault][_inToken] = _ratio; } } \ No newline at end of file diff --git a/contracts/versions/SmartVaultManagerV4.sol b/contracts/versions/SmartVaultManagerV4.sol index a6d8261..7e507ec 100644 --- a/contracts/versions/SmartVaultManagerV4.sol +++ b/contracts/versions/SmartVaultManagerV4.sol @@ -67,13 +67,13 @@ contract SmartVaultManagerV4 is ISmartVaultManager, ISmartVaultManagerV2, Initia _; } - function vaults() external view returns (SmartVaultData[] memory) { + function vaults() external view returns (SmartVaultData[] memory _vaultData) { uint256[] memory tokenIds = smartVaultIndex.getTokenIds(msg.sender); uint256 idsLength = tokenIds.length; - SmartVaultData[] memory vaultData = new SmartVaultData[](idsLength); + _vaultData = new SmartVaultData[](idsLength); for (uint256 i = 0; i < idsLength; i++) { uint256 tokenId = tokenIds[i]; - vaultData[i] = SmartVaultData({ + _vaultData[i] = SmartVaultData({ tokenId: tokenId, collateralRate: collateralRate, mintFeeRate: mintFeeRate, @@ -81,7 +81,6 @@ contract SmartVaultManagerV4 is ISmartVaultManager, ISmartVaultManagerV2, Initia status: ISmartVault(smartVaultIndex.getVaultAddress(tokenId)).status() }); } - return vaultData; } function vaultIDs(address _holder) external view returns (uint256[] memory) { diff --git a/hardhat.config.js b/hardhat.config.js index 1c00cb6..c72eca4 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -13,7 +13,16 @@ const { const testAccounts = TEST_ACCOUNT_PRIVATE_KEY ? [TEST_ACCOUNT_PRIVATE_KEY] : []; module.exports = { - solidity: "0.8.17", + solidity: { + version: "0.8.17", + settings: { + viaIR: true, + optimizer: { + enabled: true, + runs: 1, + }, + }, + }, defaultNetwork: 'hardhat', networks: { mainnet: { diff --git a/test/SmartVault.js b/test/SmartVault.js index f23af66..c155289 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -468,11 +468,13 @@ describe('SmartVault', async () => { }); describe('yield', async () => { - it('fetches empty yield list', async () => { + xit('allows deleting of yield data for a collateral type (and reverts)'); + it('fetches empty yield list', async () => { + expect(await Vault.yieldAssets()).to.be.empty; }); - it('puts all of given collateral asset into yield', async () => { + it.only('puts all of given collateral asset into yield', async () => { const WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); const CL_WBTC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC / USD'); await CL_WBTC_USD.setPrice(DEFAULT_ETH_USD_PRICE.mul(20)); @@ -489,30 +491,41 @@ describe('SmartVault', async () => { MockWeth.address, WETHGammaVaultMock.address, 500, new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [MockWeth.address, 3000, EURA.address]) ) + await YieldManager.addVaultData( + WBTC.address, WETHGammaVaultMock.address, 500, + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [WBTC.address, 3000, EURA.address]) + ) // ratio of euros vault is 1:1 - await UniProxyMock.setRatio(EUROsGammaVaultMock.address, ethers.utils.parseEther('1')); + await UniProxyMock.setRatio(EUROsGammaVaultMock.address, EURA.address, ethers.utils.parseEther('1')); // ratio of weth / wbtc vault is 1:1 in value, or 20:1 in unscaled numbers (20*10**10:1) in scaled - await UniProxyMock.setRatio(WETHGammaVaultMock.address, 5000000); + const WBTCPerETH = ethers.utils.parseUnits('0.05',8) + await UniProxyMock.setRatio(WETHGammaVaultMock.address, MockWeth.address, WBTCPerETH); + // ratio is inverse of above, 1:20 in unscaled numbers, or 1:20*10^8 + await UniProxyMock.setRatio(WETHGammaVaultMock.address, WBTC.address, ethers.utils.parseUnits('20',28)); // set fake rate for swap router: this is ETH / EUROs: ~1500 await MockSwapRouter.setRate(MockWeth.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(ethers.utils.parseEther('1')).div(DEFAULT_EUR_USD_PRICE)) // set fake rate for EURA / EURO: 1:1 await MockSwapRouter.setRate(EURA.address, EUROs.address, ethers.utils.parseEther('1')); // set fake rate for ETH / WBTC: 0.05 WBTC scaled down to 8 dec - const WBTCPerETH = 5000000 await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH); + // set fake rate for WBTC / EUROS: ~30.1k, with scaling up by 10 dec + await MockSwapRouter.setRate(WBTC.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(20).mul(ethers.utils.parseUnits('1', 28)).div(DEFAULT_EUR_USD_PRICE)) + // set fake rate for WBTC / ETH: 20 ETH scaled up by 10 dec + await MockSwapRouter.setRate(WBTC.address, MockWeth.address, ethers.utils.parseUnits('20',28)) // load up mock swap router await EURA.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); await EUROs.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); await WBTC.mint(MockSwapRouter.address, ethers.utils.parseUnits('10', 8)); + await MockWeth.mint(MockSwapRouter.address, ethers.utils.parseEther('10')); const ethCollateral = ethers.utils.parseEther('0.1') await user.sendTransaction({ to: Vault.address, value: ethCollateral }); let { collateral, totalCollateralValue } = await Vault.status(); - const preYieldCollateral = totalCollateralValue; + let preYieldCollateral = totalCollateralValue; expect(getCollateralOf('ETH', collateral).amount).to.equal(ethCollateral); await Vault.depositYield(ETH, HUNDRED_PC.div(2)); @@ -532,6 +545,17 @@ describe('SmartVault', async () => { expect(yieldAssets[1].amount0).to.equal(ethCollateral.div(4), 1); // 0.1 ETH, quarter of which should be wbtc expect(yieldAssets[1].amount1).to.be.closeTo(WBTCPerETH / 40, 1); + + // add wbtc as collateral + await WBTC.mint(Vault.address, ethers.utils.parseUnits('0.005',8)); + + ({ collateral, totalCollateralValue } = await Vault.status()); + preYieldCollateral = totalCollateralValue; + + // deposit wbtc for yield, 25% to euros pool + await Vault.depositYield(ethers.utils.formatBytes32String('WBTC'), HUNDRED_PC.div(4)); + ({ collateral, totalCollateralValue } = await Vault.status()); + expect(totalCollateralValue).to.be.closeTo(preYieldCollateral, 1); }); }); }); \ No newline at end of file From 1e6ae2da6320095f99c72bdea57e5c493dade839 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Fri, 16 Aug 2024 13:26:05 +0200 Subject: [PATCH 11/33] allows removing of vault data from vault manager, reverts --- contracts/SmartVaultYieldManager.sol | 18 ++++++--- test/SmartVault.js | 59 ++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index d90b078..4b2e337 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; +import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "contracts/interfaces/IHypervisor.sol"; import "contracts/interfaces/ISmartVaultYieldManager.sol"; @@ -10,7 +11,7 @@ import "contracts/interfaces/IWETH.sol"; import "hardhat/console.sol"; -contract SmartVaultYieldManager is ISmartVaultYieldManager { +contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { using SafeERC20 for IERC20; address private immutable EUROs; @@ -92,11 +93,11 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { } } - function swapToEURA(address _collateralToken, uint256 _euroPercentage) private { + function swapToEURA(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { uint256 _euroYieldPortion = balance(_collateralToken) * _euroPercentage / HUNDRED_PC; IERC20(_collateralToken).safeApprove(eurosRouter, _euroYieldPortion); ISwapRouter(eurosRouter).exactInput(ISwapRouter.ExactInputParams({ - path: vaultData[_collateralToken].pathToEURA, + path: _pathToEURA, recipient: address(this), deadline: block.timestamp + 60, amountIn: _euroYieldPortion, @@ -115,8 +116,8 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { IERC20(_token1).safeApprove(_vault, 0); } - function euroDeposit(address _collateralToken, uint256 _euroPercentage) private { - swapToEURA(_collateralToken, _euroPercentage); + function euroDeposit(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { + swapToEURA(_collateralToken, _euroPercentage, _pathToEURA); swapToRatio(EURA, euroVault, eurosRouter, 500); deposit(euroVault); } @@ -128,8 +129,9 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { function depositYield(address _collateralToken, uint256 _euroPercentage) external payable returns (address _vault0, address _vault1) { IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), IERC20(_collateralToken).balanceOf(address(msg.sender))); - euroDeposit(_collateralToken, _euroPercentage); VaultData memory _vaultData = vaultData[_collateralToken]; + require(_vaultData.vaultAddr != address(0), "err-invalid-request"); + euroDeposit(_collateralToken, _euroPercentage, _vaultData.pathToEURA); otherDeposit(_collateralToken, _vaultData); return (euroVault, _vaultData.vaultAddr); } @@ -137,4 +139,8 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager { function addVaultData(address _collateralToken, address _vaultAddr, uint24 _poolFee, bytes memory _EURASwapPath) external { vaultData[_collateralToken] = VaultData(_vaultAddr, _poolFee, _EURASwapPath); } + + function removeVaultData(address _collateralToken) external onlyOwner { + delete vaultData[_collateralToken]; + } } diff --git a/test/SmartVault.js b/test/SmartVault.js index c155289..488ba54 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -468,13 +468,11 @@ describe('SmartVault', async () => { }); describe('yield', async () => { - xit('allows deleting of yield data for a collateral type (and reverts)'); - it('fetches empty yield list', async () => { expect(await Vault.yieldAssets()).to.be.empty; }); - it.only('puts all of given collateral asset into yield', async () => { + it('puts all of given collateral asset into yield', async () => { const WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); const CL_WBTC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC / USD'); await CL_WBTC_USD.setPrice(DEFAULT_ETH_USD_PRICE.mul(20)); @@ -547,15 +545,68 @@ describe('SmartVault', async () => { expect(yieldAssets[1].amount1).to.be.closeTo(WBTCPerETH / 40, 1); // add wbtc as collateral - await WBTC.mint(Vault.address, ethers.utils.parseUnits('0.005',8)); + const wbtcCollateral = ethers.utils.parseUnits('0.005',8) + await WBTC.mint(Vault.address, wbtcCollateral); ({ collateral, totalCollateralValue } = await Vault.status()); + expect(getCollateralOf('WBTC', collateral).amount).to.equal(wbtcCollateral); preYieldCollateral = totalCollateralValue; // deposit wbtc for yield, 25% to euros pool await Vault.depositYield(ethers.utils.formatBytes32String('WBTC'), HUNDRED_PC.div(4)); ({ collateral, totalCollateralValue } = await Vault.status()); + expect(getCollateralOf('WBTC', collateral).amount).to.equal(0); expect(totalCollateralValue).to.be.closeTo(preYieldCollateral, 1); }); + + it('allows deleting of yield data for a collateral type (and reverts)', async () => { + const WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); + const CL_WBTC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC / USD'); + await CL_WBTC_USD.setPrice(DEFAULT_ETH_USD_PRICE.mul(20)); + await TokenManager.addAcceptedToken(WBTC.address, CL_WBTC_USD.address); + await TokenManager.addAcceptedToken(MockWeth.address, ClEthUsd.address); + + // fake gamma vault for WETH + WBTC + const WETHGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( + 'WETH-WBTC', 'WETH-WBTC', MockWeth.address, WBTC.address + ); + + // data about how yield manager converts collateral to EURA, vault addresses etc + await YieldManager.addVaultData( + MockWeth.address, WETHGammaVaultMock.address, 500, + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [MockWeth.address, 3000, EURA.address]) + ) + + // ratio of euros vault is 1:1 + await UniProxyMock.setRatio(EUROsGammaVaultMock.address, EURA.address, ethers.utils.parseEther('1')); + // ratio of weth / wbtc vault is 1:1 in value, or 20:1 in unscaled numbers (20*10**10:1) in scaled + const WBTCPerETH = ethers.utils.parseUnits('0.05',8) + await UniProxyMock.setRatio(WETHGammaVaultMock.address, MockWeth.address, WBTCPerETH); + // ratio is inverse of above, 1:20 in unscaled numbers, or 1:20*10^8 + await UniProxyMock.setRatio(WETHGammaVaultMock.address, WBTC.address, ethers.utils.parseUnits('20',28)); + + // set fake rate for swap router: this is ETH / EUROs: ~1500 + await MockSwapRouter.setRate(MockWeth.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(ethers.utils.parseEther('1')).div(DEFAULT_EUR_USD_PRICE)) + // set fake rate for EURA / EURO: 1:1 + await MockSwapRouter.setRate(EURA.address, EUROs.address, ethers.utils.parseEther('1')); + // set fake rate for ETH / WBTC: 0.05 WBTC scaled down to 8 dec + await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH); + + // load up mock swap router + await EURA.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); + await EUROs.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); + await WBTC.mint(MockSwapRouter.address, ethers.utils.parseUnits('10', 8)); + + const ethCollateral = ethers.utils.parseEther('0.1') + await user.sendTransaction({ to: Vault.address, value: ethCollateral }); + + await expect(Vault.depositYield(ETH, HUNDRED_PC.div(2))).not.to.be.reverted; + + await expect(YieldManager.connect(user).removeVaultData(MockWeth.address)).to.be.revertedWith('Ownable: caller is not the owner'); + await expect(YieldManager.connect(admin).removeVaultData(MockWeth.address)).not.to.be.reverted; + + await user.sendTransaction({ to: Vault.address, value: ethCollateral }); + await expect(Vault.depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWith('err-invalid-request'); + }); }); }); \ No newline at end of file From 8b7becd27c8e480dacb5fae90c2303e6832bb5dd Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 13:20:02 +0200 Subject: [PATCH 12/33] protect yield functions for owner only --- contracts/SmartVaultV4.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index 860b17f..b6bce09 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -35,7 +35,7 @@ contract SmartVaultV4 is ISmartVault { event EUROsMinted(address to, uint256 amount, uint256 fee); event EUROsBurned(uint256 amount, uint256 fee); - struct YieldPair { address token0; uint256 amount0; address token1; uint256 amount1; } + struct YieldPair { address vault; address token0; uint256 amount0; address token1; uint256 amount1; } error InvalidUser(); error InvalidRequest(); @@ -280,7 +280,7 @@ contract SmartVaultV4 is ISmartVault { vaultTokens.push(_vault); } - function depositYield(bytes32 _symbol, uint256 _euroPercentage) external { + function depositYield(bytes32 _symbol, uint256 _euroPercentage) external onlyOwner { if (_symbol == NATIVE) IWETH(ISmartVaultManagerV3(manager).weth()).deposit{value: address(this).balance}(); address _token = getTokenisedAddr(_symbol); uint256 _balance = getAssetBalance(_token); @@ -291,6 +291,11 @@ contract SmartVaultV4 is ISmartVault { addUniqueVaultToken(_vault2); } + function withdrawYield(address _vault, bytes32 _symbol) external onlyOwner { + address _token = getTokenisedAddr(_symbol); + ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).withdrawYield(_vault, _token); + } + function yieldAssets() external view returns (YieldPair[] memory _yieldPairs) { _yieldPairs = new YieldPair[](vaultTokens.length); for (uint256 i = 0; i < vaultTokens.length; i++) { @@ -299,6 +304,7 @@ contract SmartVaultV4 is ISmartVault { uint256 _vaultTotal = _vaultToken.totalSupply(); (uint256 _underlyingTotal0, uint256 _underlyingTotal1) = _vaultToken.getTotalAmounts(); + _yieldPairs[i].vault = vaultTokens[i]; _yieldPairs[i].token0 = _vaultToken.token0(); _yieldPairs[i].token1 = _vaultToken.token1(); _yieldPairs[i].amount0 = _balance * _underlyingTotal0 / _vaultTotal; From bed96f42e056fafba93fdd9deddf2770931c6628 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 13:20:27 +0200 Subject: [PATCH 13/33] add withdraw yield function --- contracts/SmartVaultYieldManager.sol | 6 +++++- contracts/interfaces/ISmartVaultYieldManager.sol | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index 4b2e337..cdcb364 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -127,7 +127,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { deposit(_vaultData.vaultAddr); } - function depositYield(address _collateralToken, uint256 _euroPercentage) external payable returns (address _vault0, address _vault1) { + function depositYield(address _collateralToken, uint256 _euroPercentage) external returns (address _vault0, address _vault1) { IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), IERC20(_collateralToken).balanceOf(address(msg.sender))); VaultData memory _vaultData = vaultData[_collateralToken]; require(_vaultData.vaultAddr != address(0), "err-invalid-request"); @@ -136,6 +136,10 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { return (euroVault, _vaultData.vaultAddr); } + function withdrawYield(address _vault, address _token) external { + + } + function addVaultData(address _collateralToken, address _vaultAddr, uint24 _poolFee, bytes memory _EURASwapPath) external { vaultData[_collateralToken] = VaultData(_vaultAddr, _poolFee, _EURASwapPath); } diff --git a/contracts/interfaces/ISmartVaultYieldManager.sol b/contracts/interfaces/ISmartVaultYieldManager.sol index 1892e33..673e4fd 100644 --- a/contracts/interfaces/ISmartVaultYieldManager.sol +++ b/contracts/interfaces/ISmartVaultYieldManager.sol @@ -2,5 +2,6 @@ pragma solidity 0.8.17; interface ISmartVaultYieldManager { - function depositYield(address _collateralToken, uint256 _euroPercentage) external payable returns (address vault0, address vault1); + function depositYield(address _collateralToken, uint256 _euroPercentage) external returns (address vault0, address vault1); + function withdrawYield(address _vault, address _token) external; } \ No newline at end of file From 25474664b2757de6bc9450f0e44e57efea36627f Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 13:20:34 +0200 Subject: [PATCH 14/33] protect yield functions for owner only --- test/SmartVault.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/SmartVault.js b/test/SmartVault.js index 488ba54..b2cdbbf 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -526,7 +526,10 @@ describe('SmartVault', async () => { let preYieldCollateral = totalCollateralValue; expect(getCollateralOf('ETH', collateral).amount).to.equal(ethCollateral); - await Vault.depositYield(ETH, HUNDRED_PC.div(2)); + // only vault owner can deposit collateral as yield + await expect(Vault.connect(admin).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWithCustomError(Vault, 'InvalidUser'); + await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).not.to.be.reverted; + ({ collateral, totalCollateralValue } = await Vault.status()); expect(getCollateralOf('ETH', collateral).amount).to.equal(0); // allow a delta of 2 wei in pre and post yield collateral, due to dividing etc @@ -553,7 +556,7 @@ describe('SmartVault', async () => { preYieldCollateral = totalCollateralValue; // deposit wbtc for yield, 25% to euros pool - await Vault.depositYield(ethers.utils.formatBytes32String('WBTC'), HUNDRED_PC.div(4)); + await Vault.connect(user).depositYield(ethers.utils.formatBytes32String('WBTC'), HUNDRED_PC.div(4)); ({ collateral, totalCollateralValue } = await Vault.status()); expect(getCollateralOf('WBTC', collateral).amount).to.equal(0); expect(totalCollateralValue).to.be.closeTo(preYieldCollateral, 1); @@ -600,13 +603,13 @@ describe('SmartVault', async () => { const ethCollateral = ethers.utils.parseEther('0.1') await user.sendTransaction({ to: Vault.address, value: ethCollateral }); - await expect(Vault.depositYield(ETH, HUNDRED_PC.div(2))).not.to.be.reverted; + await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).not.to.be.reverted; await expect(YieldManager.connect(user).removeVaultData(MockWeth.address)).to.be.revertedWith('Ownable: caller is not the owner'); await expect(YieldManager.connect(admin).removeVaultData(MockWeth.address)).not.to.be.reverted; await user.sendTransaction({ to: Vault.address, value: ethCollateral }); - await expect(Vault.depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWith('err-invalid-request'); + await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWith('err-invalid-request'); }); }); }); \ No newline at end of file From 8ba3f45d0b270c9c690f4fadd719b0370314398e Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 14:21:13 +0200 Subject: [PATCH 15/33] refactor yield test --- test/SmartVault.js | 57 +++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/test/SmartVault.js b/test/SmartVault.js index b2cdbbf..92545f3 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -467,13 +467,11 @@ describe('SmartVault', async () => { }); }); - describe('yield', async () => { - it('fetches empty yield list', async () => { - expect(await Vault.yieldAssets()).to.be.empty; - }); + describe.only('yield', async () => { + let WBTC, WBTCPerETH; - it('puts all of given collateral asset into yield', async () => { - const WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); + beforeEach(async () => { + WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); const CL_WBTC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC / USD'); await CL_WBTC_USD.setPrice(DEFAULT_ETH_USD_PRICE.mul(20)); await TokenManager.addAcceptedToken(WBTC.address, CL_WBTC_USD.address); @@ -497,7 +495,7 @@ describe('SmartVault', async () => { // ratio of euros vault is 1:1 await UniProxyMock.setRatio(EUROsGammaVaultMock.address, EURA.address, ethers.utils.parseEther('1')); // ratio of weth / wbtc vault is 1:1 in value, or 20:1 in unscaled numbers (20*10**10:1) in scaled - const WBTCPerETH = ethers.utils.parseUnits('0.05',8) + WBTCPerETH = ethers.utils.parseUnits('0.05',8) await UniProxyMock.setRatio(WETHGammaVaultMock.address, MockWeth.address, WBTCPerETH); // ratio is inverse of above, 1:20 in unscaled numbers, or 1:20*10^8 await UniProxyMock.setRatio(WETHGammaVaultMock.address, WBTC.address, ethers.utils.parseUnits('20',28)); @@ -518,7 +516,13 @@ describe('SmartVault', async () => { await EUROs.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); await WBTC.mint(MockSwapRouter.address, ethers.utils.parseUnits('10', 8)); await MockWeth.mint(MockSwapRouter.address, ethers.utils.parseEther('10')); + }); + it('fetches empty yield list', async () => { + expect(await Vault.yieldAssets()).to.be.empty; + }); + + it('puts all of given collateral asset into yield', async () => { const ethCollateral = ethers.utils.parseEther('0.1') await user.sendTransaction({ to: Vault.address, value: ethCollateral }); @@ -563,38 +567,6 @@ describe('SmartVault', async () => { }); it('allows deleting of yield data for a collateral type (and reverts)', async () => { - const WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); - const CL_WBTC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC / USD'); - await CL_WBTC_USD.setPrice(DEFAULT_ETH_USD_PRICE.mul(20)); - await TokenManager.addAcceptedToken(WBTC.address, CL_WBTC_USD.address); - await TokenManager.addAcceptedToken(MockWeth.address, ClEthUsd.address); - - // fake gamma vault for WETH + WBTC - const WETHGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( - 'WETH-WBTC', 'WETH-WBTC', MockWeth.address, WBTC.address - ); - - // data about how yield manager converts collateral to EURA, vault addresses etc - await YieldManager.addVaultData( - MockWeth.address, WETHGammaVaultMock.address, 500, - new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [MockWeth.address, 3000, EURA.address]) - ) - - // ratio of euros vault is 1:1 - await UniProxyMock.setRatio(EUROsGammaVaultMock.address, EURA.address, ethers.utils.parseEther('1')); - // ratio of weth / wbtc vault is 1:1 in value, or 20:1 in unscaled numbers (20*10**10:1) in scaled - const WBTCPerETH = ethers.utils.parseUnits('0.05',8) - await UniProxyMock.setRatio(WETHGammaVaultMock.address, MockWeth.address, WBTCPerETH); - // ratio is inverse of above, 1:20 in unscaled numbers, or 1:20*10^8 - await UniProxyMock.setRatio(WETHGammaVaultMock.address, WBTC.address, ethers.utils.parseUnits('20',28)); - - // set fake rate for swap router: this is ETH / EUROs: ~1500 - await MockSwapRouter.setRate(MockWeth.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(ethers.utils.parseEther('1')).div(DEFAULT_EUR_USD_PRICE)) - // set fake rate for EURA / EURO: 1:1 - await MockSwapRouter.setRate(EURA.address, EUROs.address, ethers.utils.parseEther('1')); - // set fake rate for ETH / WBTC: 0.05 WBTC scaled down to 8 dec - await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH); - // load up mock swap router await EURA.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); await EUROs.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); @@ -611,5 +583,12 @@ describe('SmartVault', async () => { await user.sendTransaction({ to: Vault.address, value: ethCollateral }); await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWith('err-invalid-request'); }); + + it('withdraw yield deposits by vault', async () => { + + }); + + xit('reverts if collateral level falls below required level'); + xit('reverts if withdrawal asset is not compatible with yield vault'); }); }); \ No newline at end of file From ed85bbcf4ef13be268d682eeedfd158d5a5e88f3 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 14:21:34 +0200 Subject: [PATCH 16/33] remove yield test focus --- test/SmartVault.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/SmartVault.js b/test/SmartVault.js index 92545f3..b971cea 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -467,7 +467,7 @@ describe('SmartVault', async () => { }); }); - describe.only('yield', async () => { + describe('yield', async () => { let WBTC, WBTCPerETH; beforeEach(async () => { From 26522965bbfb82be56d835f82a30c9be64791248 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 14:39:09 +0200 Subject: [PATCH 17/33] withdraw test --- test/SmartVault.js | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/test/SmartVault.js b/test/SmartVault.js index b971cea..5cd202e 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -468,7 +468,7 @@ describe('SmartVault', async () => { }); describe('yield', async () => { - let WBTC, WBTCPerETH; + let WBTC, WBTCPerETH, WETHGammaVaultMock; beforeEach(async () => { WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); @@ -478,7 +478,7 @@ describe('SmartVault', async () => { await TokenManager.addAcceptedToken(MockWeth.address, ClEthUsd.address); // fake gamma vault for WETH + WBTC - const WETHGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( + WETHGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( 'WETH-WBTC', 'WETH-WBTC', MockWeth.address, WBTC.address ); @@ -564,14 +564,10 @@ describe('SmartVault', async () => { ({ collateral, totalCollateralValue } = await Vault.status()); expect(getCollateralOf('WBTC', collateral).amount).to.equal(0); expect(totalCollateralValue).to.be.closeTo(preYieldCollateral, 1); + // TODO assertions on the yield assets for wbtc deposit }); it('allows deleting of yield data for a collateral type (and reverts)', async () => { - // load up mock swap router - await EURA.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); - await EUROs.mint(MockSwapRouter.address, ethers.utils.parseEther('1000000')); - await WBTC.mint(MockSwapRouter.address, ethers.utils.parseUnits('10', 8)); - const ethCollateral = ethers.utils.parseEther('0.1') await user.sendTransaction({ to: Vault.address, value: ethCollateral }); @@ -584,8 +580,26 @@ describe('SmartVault', async () => { await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWith('err-invalid-request'); }); - it('withdraw yield deposits by vault', async () => { + it.only('withdraw yield deposits by vault', async () => { + const ethCollateral = ethers.utils.parseEther('0.1'); + await user.sendTransaction({ to: Vault.address, value: ethCollateral }); + // 25% yield to stable pool + await Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(4)); + expect(await Vault.yieldAssets()).to.have.length(2); + const status = await Vault.status(); + const preWithdrawCollateralValue = status.totalCollateralValue; + expect(getCollateralOf('ETH', status.collateral).amount).to.equal(0); + const [ EUROsYield ] = await Vault.yieldAssets(); + + await Vault.connect(user).withdrawYield(EUROsYield.vault, ETH); + const { totalCollateralValue, collateral } = await Vault.status(); + expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 1); + const yieldAssets = await Vault.yieldAssets(); + expect(yieldAssets).to.have.length(1); + expect(yieldAssets[0].vault).to.equal(WETHGammaVaultMock.address); + // should have withdrawn ~quarter of eth collateral, because that much was put in stable pool originally + expect(getCollateralOf('ETH', collateral)).to.equal(ethCollateral.div(4)); }); xit('reverts if collateral level falls below required level'); From 3e42ce6ec50279bda4298bbcfd29888c0d98d84f Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 16:09:05 +0200 Subject: [PATCH 18/33] withdrawal from EUROs vault --- contracts/SmartVaultV4.sol | 18 +++++- contracts/SmartVaultYieldManager.sol | 55 ++++++++++++++++--- contracts/interfaces/IHypervisor.sol | 7 +++ .../interfaces/ISmartVaultYieldManager.sol | 4 +- contracts/test_utils/GammaVaultMock.sol | 16 +++++- test/SmartVault.js | 17 ++++-- 6 files changed, 98 insertions(+), 19 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index b6bce09..1e37852 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -280,20 +280,34 @@ contract SmartVaultV4 is ISmartVault { vaultTokens.push(_vault); } + function removeVaultToken(address _vault) private { + for (uint256 i = 0; i < vaultTokens.length; i++) { + if (vaultTokens[i] == _vault) { + vaultTokens[i] = vaultTokens[vaultTokens.length - 1]; + vaultTokens.pop(); + } + } + } + function depositYield(bytes32 _symbol, uint256 _euroPercentage) external onlyOwner { if (_symbol == NATIVE) IWETH(ISmartVaultManagerV3(manager).weth()).deposit{value: address(this).balance}(); address _token = getTokenisedAddr(_symbol); uint256 _balance = getAssetBalance(_token); if (_balance == 0) revert InvalidRequest(); IERC20(_token).safeApprove(ISmartVaultManagerV3(manager).yieldManager(), _balance); - (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).depositYield(_token, _euroPercentage); + (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).deposit(_token, _euroPercentage); addUniqueVaultToken(_vault1); addUniqueVaultToken(_vault2); } function withdrawYield(address _vault, bytes32 _symbol) external onlyOwner { address _token = getTokenisedAddr(_symbol); - ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).withdrawYield(_vault, _token); + IERC20(_vault).safeApprove(ISmartVaultManagerV3(manager).yieldManager(), IERC20(_vault).balanceOf(address(this))); + ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).withdraw(_vault, _token); + removeVaultToken(_vault); + if (_symbol == NATIVE) { + IWETH(_token).withdraw(getAssetBalance(_token)); + } } function yieldAssets() external view returns (YieldPair[] memory _yieldPairs) { diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index cdcb364..b5b4076 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -24,7 +24,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { uint256 private constant HUNDRED_PC = 1e5; mapping(address => VaultData) private vaultData; - struct VaultData { address vaultAddr; uint24 poolFee; bytes pathToEURA; } + struct VaultData { address vaultAddr; uint24 poolFee; bytes pathToEURA; bytes pathFromEURA; } constructor(address _EUROs, address _EURA, address _WETH, address _uniProxy, address _eurosRouter, address _euroVault, address _uniswapRouter) { EUROs = _EUROs; @@ -95,15 +95,15 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { function swapToEURA(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { uint256 _euroYieldPortion = balance(_collateralToken) * _euroPercentage / HUNDRED_PC; - IERC20(_collateralToken).safeApprove(eurosRouter, _euroYieldPortion); - ISwapRouter(eurosRouter).exactInput(ISwapRouter.ExactInputParams({ + IERC20(_collateralToken).safeApprove(uniswapRouter, _euroYieldPortion); + ISwapRouter(uniswapRouter).exactInput(ISwapRouter.ExactInputParams({ path: _pathToEURA, recipient: address(this), deadline: block.timestamp + 60, amountIn: _euroYieldPortion, amountOutMinimum: 1 })); - IERC20(_collateralToken).safeApprove(eurosRouter, 0); + IERC20(_collateralToken).safeApprove(uniswapRouter, 0); } function deposit(address _vault) private { @@ -127,7 +127,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { deposit(_vaultData.vaultAddr); } - function depositYield(address _collateralToken, uint256 _euroPercentage) external returns (address _vault0, address _vault1) { + function deposit(address _collateralToken, uint256 _euroPercentage) external returns (address _vault0, address _vault1) { IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), IERC20(_collateralToken).balanceOf(address(msg.sender))); VaultData memory _vaultData = vaultData[_collateralToken]; require(_vaultData.vaultAddr != address(0), "err-invalid-request"); @@ -136,12 +136,51 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { return (euroVault, _vaultData.vaultAddr); } - function withdrawYield(address _vault, address _token) external { + function sellEUROs() private { + uint256 _balance = balance(EUROs); + IERC20(EUROs).safeApprove(eurosRouter, _balance); + ISwapRouter(eurosRouter).exactInputSingle(ISwapRouter.ExactInputSingleParams({ + tokenIn: EUROs, + tokenOut: EURA, + fee: 500, + recipient: address(this), + deadline: block.timestamp + 60, + amountIn: _balance, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + })); + IERC20(EUROs).safeApprove(eurosRouter, 0); + } + function sellEURA(address _token) private { + bytes memory _pathFromEURA = vaultData[_token].pathFromEURA; + uint256 _balance = balance(EURA); + IERC20(EURA).safeApprove(uniswapRouter, _balance); + ISwapRouter(uniswapRouter).exactInput(ISwapRouter.ExactInputParams({ + path: _pathFromEURA, + recipient: msg.sender, + deadline: block.timestamp + 60, + amountIn: _balance, + amountOutMinimum: 0 + })); + IERC20(EUROs).safeApprove(uniswapRouter, 0); + } + + function withdrawEUROsDeposit(address _token) private { + IHypervisor(euroVault).withdraw(balance(euroVault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); + sellEUROs(); + sellEURA(_token); + } + + function withdraw(address _vault, address _token) external { + IERC20(_vault).safeTransferFrom(msg.sender, address(this), IERC20(_vault).balanceOf(msg.sender)); + if (_vault == euroVault) { + withdrawEUROsDeposit(_token); + } } - function addVaultData(address _collateralToken, address _vaultAddr, uint24 _poolFee, bytes memory _EURASwapPath) external { - vaultData[_collateralToken] = VaultData(_vaultAddr, _poolFee, _EURASwapPath); + function addVaultData(address _collateralToken, address _vaultAddr, uint24 _poolFee, bytes memory _pathToEURA, bytes memory _pathFromEURA) external { + vaultData[_collateralToken] = VaultData(_vaultAddr, _poolFee, _pathToEURA, _pathFromEURA); } function removeVaultData(address _collateralToken) external onlyOwner { diff --git a/contracts/interfaces/IHypervisor.sol b/contracts/interfaces/IHypervisor.sol index 83a7a57..21c2fd0 100644 --- a/contracts/interfaces/IHypervisor.sol +++ b/contracts/interfaces/IHypervisor.sol @@ -14,4 +14,11 @@ interface IHypervisor is IERC20 { address from, uint256[4] memory inMin ) external returns (uint256 shares); + + function withdraw( + uint256 shares, + address to, + address from, + uint256[4] memory minAmounts + ) external returns (uint256 amount0, uint256 amount1); } \ No newline at end of file diff --git a/contracts/interfaces/ISmartVaultYieldManager.sol b/contracts/interfaces/ISmartVaultYieldManager.sol index 673e4fd..4ead18f 100644 --- a/contracts/interfaces/ISmartVaultYieldManager.sol +++ b/contracts/interfaces/ISmartVaultYieldManager.sol @@ -2,6 +2,6 @@ pragma solidity 0.8.17; interface ISmartVaultYieldManager { - function depositYield(address _collateralToken, uint256 _euroPercentage) external returns (address vault0, address vault1); - function withdrawYield(address _vault, address _token) external; + function deposit(address _collateralToken, uint256 _euroPercentage) external returns (address vault0, address vault1); + function withdraw(address _vault, address _token) external; } \ No newline at end of file diff --git a/contracts/test_utils/GammaVaultMock.sol b/contracts/test_utils/GammaVaultMock.sol index 24d88b3..2b4a297 100644 --- a/contracts/test_utils/GammaVaultMock.sol +++ b/contracts/test_utils/GammaVaultMock.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.17; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -// import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "contracts/interfaces/IHypervisor.sol"; import "hardhat/console.sol"; @@ -33,4 +33,18 @@ contract GammaVaultMock is IHypervisor, ERC20 { // simplified calculation because our mock will not deal with a changing swap rate _mint(to, deposit0); } + + function withdraw( + uint256 shares, + address to, + address from, + uint256[4] memory minAmounts + ) external returns (uint256 amount0, uint256 amount1) { + (uint256 _total0, uint256 _total1) = getTotalAmounts(); + amount0 = shares * _total0 / totalSupply(); + amount1 = shares * _total1 / totalSupply(); + _burn(from, shares); + IERC20(token0).transfer(to, amount0); + IERC20(token1).transfer(to, amount1); + } } \ No newline at end of file diff --git a/test/SmartVault.js b/test/SmartVault.js index 5cd202e..7991731 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -485,11 +485,13 @@ describe('SmartVault', async () => { // data about how yield manager converts collateral to EURA, vault addresses etc await YieldManager.addVaultData( MockWeth.address, WETHGammaVaultMock.address, 500, - new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [MockWeth.address, 3000, EURA.address]) + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [MockWeth.address, 3000, EURA.address]), + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, MockWeth.address]) ) await YieldManager.addVaultData( WBTC.address, WETHGammaVaultMock.address, 500, - new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [WBTC.address, 3000, EURA.address]) + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [WBTC.address, 3000, EURA.address]), + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, WBTC.address]) ) // ratio of euros vault is 1:1 @@ -501,9 +503,11 @@ describe('SmartVault', async () => { await UniProxyMock.setRatio(WETHGammaVaultMock.address, WBTC.address, ethers.utils.parseUnits('20',28)); // set fake rate for swap router: this is ETH / EUROs: ~1500 - await MockSwapRouter.setRate(MockWeth.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(ethers.utils.parseEther('1')).div(DEFAULT_EUR_USD_PRICE)) + await MockSwapRouter.setRate(MockWeth.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(ethers.utils.parseEther('1')).div(DEFAULT_EUR_USD_PRICE)); + await MockSwapRouter.setRate(EURA.address, MockWeth.address, ethers.utils.parseEther('1').mul(DEFAULT_EUR_USD_PRICE).div(DEFAULT_ETH_USD_PRICE)); // set fake rate for EURA / EURO: 1:1 await MockSwapRouter.setRate(EURA.address, EUROs.address, ethers.utils.parseEther('1')); + await MockSwapRouter.setRate(EUROs.address, EURA.address, ethers.utils.parseEther('1')); // set fake rate for ETH / WBTC: 0.05 WBTC scaled down to 8 dec await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH); // set fake rate for WBTC / EUROS: ~30.1k, with scaling up by 10 dec @@ -580,7 +584,7 @@ describe('SmartVault', async () => { await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWith('err-invalid-request'); }); - it.only('withdraw yield deposits by vault', async () => { + it('withdraw yield deposits by vault', async () => { const ethCollateral = ethers.utils.parseEther('0.1'); await user.sendTransaction({ to: Vault.address, value: ethCollateral }); @@ -594,12 +598,13 @@ describe('SmartVault', async () => { await Vault.connect(user).withdrawYield(EUROsYield.vault, ETH); const { totalCollateralValue, collateral } = await Vault.status(); - expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 1); + // fake rate from swap router causing a slight accuracy area + expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 2000); const yieldAssets = await Vault.yieldAssets(); expect(yieldAssets).to.have.length(1); expect(yieldAssets[0].vault).to.equal(WETHGammaVaultMock.address); // should have withdrawn ~quarter of eth collateral, because that much was put in stable pool originally - expect(getCollateralOf('ETH', collateral)).to.equal(ethCollateral.div(4)); + expect(getCollateralOf('ETH', collateral).amount).to.be.closeTo(ethCollateral.div(4), 1); }); xit('reverts if collateral level falls below required level'); From ca14f5b7d0eec2f9027615c5bacb371e296f6797 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 16:22:09 +0200 Subject: [PATCH 19/33] underscore private functions for clarity --- contracts/SmartVaultYieldManager.sol | 60 ++++++++++++++-------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index b5b4076..71a3506 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -36,15 +36,15 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { uniswapRouter = _uniswapRouter; } - function balance(address _token) private view returns (uint256) { + function _thisBalanceOf(address _token) private view returns (uint256) { return IERC20(_token).balanceOf(address(this)); } - function swapToRatio(address _tokenA, address _hypervisor, address _swapRouter, uint24 _fee) private { + function _swapToRatio(address _tokenA, address _hypervisor, address _swapRouter, uint24 _fee) private { address _tokenB = _tokenA == IHypervisor(_hypervisor).token0() ? IHypervisor(_hypervisor).token1() : IHypervisor(_hypervisor).token0(); - uint256 _tokenBBalance = balance(_tokenB); - (uint256 amountStart, uint256 amountEnd) = IUniProxy(uniProxy).getDepositAmount(_hypervisor, _tokenA, balance(_tokenA)); + uint256 _tokenBBalance = _thisBalanceOf(_tokenB); + (uint256 amountStart, uint256 amountEnd) = IUniProxy(uniProxy).getDepositAmount(_hypervisor, _tokenA, _thisBalanceOf(_tokenA)); uint256 _divisor = 2; bool _tokenBTooLarge; while(_tokenBBalance < amountStart || _tokenBBalance > amountEnd) { @@ -54,7 +54,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { _divisor++; _tokenBTooLarge = false; } - IERC20(_tokenA).safeApprove(_swapRouter, balance(_tokenA)); + IERC20(_tokenA).safeApprove(_swapRouter, _thisBalanceOf(_tokenA)); try ISwapRouter(_swapRouter).exactOutputSingle(ISwapRouter.ExactOutputSingleParams({ tokenIn: _tokenA, tokenOut: _tokenB, @@ -62,7 +62,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { recipient: address(this), deadline: block.timestamp + 60, amountOut: (_midRatio - _tokenBBalance) / _divisor, - amountInMaximum: balance(_tokenA), + amountInMaximum: _thisBalanceOf(_tokenA), sqrtPriceLimitX96: 0 })) returns (uint256) {} catch { _divisor++; @@ -88,13 +88,13 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { } IERC20(_tokenB).safeApprove(_swapRouter, 0); } - _tokenBBalance = balance(_tokenB); - (amountStart, amountEnd) = IUniProxy(uniProxy).getDepositAmount(_hypervisor, _tokenA, balance(_tokenA)); + _tokenBBalance = _thisBalanceOf(_tokenB); + (amountStart, amountEnd) = IUniProxy(uniProxy).getDepositAmount(_hypervisor, _tokenA, _thisBalanceOf(_tokenA)); } } - function swapToEURA(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { - uint256 _euroYieldPortion = balance(_collateralToken) * _euroPercentage / HUNDRED_PC; + function _swapToEURA(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { + uint256 _euroYieldPortion = _thisBalanceOf(_collateralToken) * _euroPercentage / HUNDRED_PC; IERC20(_collateralToken).safeApprove(uniswapRouter, _euroYieldPortion); ISwapRouter(uniswapRouter).exactInput(ISwapRouter.ExactInputParams({ path: _pathToEURA, @@ -106,38 +106,38 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { IERC20(_collateralToken).safeApprove(uniswapRouter, 0); } - function deposit(address _vault) private { + function _deposit(address _vault) private { address _token0 = IHypervisor(_vault).token0(); address _token1 = IHypervisor(_vault).token1(); - IERC20(_token0).safeApprove(_vault, balance(_token0)); - IERC20(_token1).safeApprove(_vault, balance(_token1)); - IUniProxy(uniProxy).deposit(balance(_token0), balance(_token1), msg.sender, _vault, [uint256(0),uint256(0),uint256(0),uint256(0)]); + IERC20(_token0).safeApprove(_vault, _thisBalanceOf(_token0)); + IERC20(_token1).safeApprove(_vault, _thisBalanceOf(_token1)); + IUniProxy(uniProxy).deposit(_thisBalanceOf(_token0), _thisBalanceOf(_token1), msg.sender, _vault, [uint256(0),uint256(0),uint256(0),uint256(0)]); IERC20(_token0).safeApprove(_vault, 0); IERC20(_token1).safeApprove(_vault, 0); } - function euroDeposit(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { - swapToEURA(_collateralToken, _euroPercentage, _pathToEURA); - swapToRatio(EURA, euroVault, eurosRouter, 500); - deposit(euroVault); + function _euroDeposit(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { + _swapToEURA(_collateralToken, _euroPercentage, _pathToEURA); + _swapToRatio(EURA, euroVault, eurosRouter, 500); + _deposit(euroVault); } - function otherDeposit(address _collateralToken, VaultData memory _vaultData) private { - swapToRatio(_collateralToken, _vaultData.vaultAddr, uniswapRouter, _vaultData.poolFee); - deposit(_vaultData.vaultAddr); + function _otherDeposit(address _collateralToken, VaultData memory _vaultData) private { + _swapToRatio(_collateralToken, _vaultData.vaultAddr, uniswapRouter, _vaultData.poolFee); + _deposit(_vaultData.vaultAddr); } function deposit(address _collateralToken, uint256 _euroPercentage) external returns (address _vault0, address _vault1) { IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), IERC20(_collateralToken).balanceOf(address(msg.sender))); VaultData memory _vaultData = vaultData[_collateralToken]; require(_vaultData.vaultAddr != address(0), "err-invalid-request"); - euroDeposit(_collateralToken, _euroPercentage, _vaultData.pathToEURA); - otherDeposit(_collateralToken, _vaultData); + _euroDeposit(_collateralToken, _euroPercentage, _vaultData.pathToEURA); + _otherDeposit(_collateralToken, _vaultData); return (euroVault, _vaultData.vaultAddr); } - function sellEUROs() private { - uint256 _balance = balance(EUROs); + function _sellEUROs() private { + uint256 _balance = _thisBalanceOf(EUROs); IERC20(EUROs).safeApprove(eurosRouter, _balance); ISwapRouter(eurosRouter).exactInputSingle(ISwapRouter.ExactInputSingleParams({ tokenIn: EUROs, @@ -152,9 +152,9 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { IERC20(EUROs).safeApprove(eurosRouter, 0); } - function sellEURA(address _token) private { + function _sellEURA(address _token) private { bytes memory _pathFromEURA = vaultData[_token].pathFromEURA; - uint256 _balance = balance(EURA); + uint256 _balance = _thisBalanceOf(EURA); IERC20(EURA).safeApprove(uniswapRouter, _balance); ISwapRouter(uniswapRouter).exactInput(ISwapRouter.ExactInputParams({ path: _pathFromEURA, @@ -167,9 +167,9 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { } function withdrawEUROsDeposit(address _token) private { - IHypervisor(euroVault).withdraw(balance(euroVault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); - sellEUROs(); - sellEURA(_token); + IHypervisor(euroVault).withdraw(_thisBalanceOf(euroVault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); + _sellEUROs(); + _sellEURA(_token); } function withdraw(address _vault, address _token) external { From 123c452f111e69783f667fde95b07a31837ae470 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 16:49:05 +0200 Subject: [PATCH 20/33] withdrawal from other vaults --- contracts/SmartVaultYieldManager.sol | 56 ++++++++++++++++++++++------ test/SmartVault.js | 27 +++++++++----- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index 71a3506..b2bc98f 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -24,7 +24,9 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { uint256 private constant HUNDRED_PC = 1e5; mapping(address => VaultData) private vaultData; - struct VaultData { address vaultAddr; uint24 poolFee; bytes pathToEURA; bytes pathFromEURA; } + struct VaultData { address vault; uint24 poolFee; bytes pathToEURA; bytes pathFromEURA; } + + error InvalidRequest(); constructor(address _EUROs, address _EURA, address _WETH, address _uniProxy, address _eurosRouter, address _euroVault, address _uniswapRouter) { EUROs = _EUROs; @@ -93,6 +95,26 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { } } + function _swapToSingleAsset(address _vault, address _wantedToken, address _swapRouter, uint24 _fee) private { + address _token0 = IHypervisor(_vault).token0(); + address _unwantedToken = IHypervisor(_vault).token0() == _wantedToken ? + IHypervisor(_vault).token1() : + _token0; + uint256 _balance = _thisBalanceOf(_unwantedToken); + IERC20(_unwantedToken).safeApprove(_swapRouter, _balance); + ISwapRouter(_swapRouter).exactInputSingle(ISwapRouter.ExactInputSingleParams({ + tokenIn: _unwantedToken, + tokenOut: _wantedToken, + fee: _fee, + recipient: address(this), + deadline: block.timestamp + 60, + amountIn: _balance, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + })); + IERC20(_unwantedToken).safeApprove(_swapRouter, 0); + } + function _swapToEURA(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { uint256 _euroYieldPortion = _thisBalanceOf(_collateralToken) * _euroPercentage / HUNDRED_PC; IERC20(_collateralToken).safeApprove(uniswapRouter, _euroYieldPortion); @@ -123,17 +145,18 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { } function _otherDeposit(address _collateralToken, VaultData memory _vaultData) private { - _swapToRatio(_collateralToken, _vaultData.vaultAddr, uniswapRouter, _vaultData.poolFee); - _deposit(_vaultData.vaultAddr); + _swapToRatio(_collateralToken, _vaultData.vault, uniswapRouter, _vaultData.poolFee); + _deposit(_vaultData.vault); } function deposit(address _collateralToken, uint256 _euroPercentage) external returns (address _vault0, address _vault1) { IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), IERC20(_collateralToken).balanceOf(address(msg.sender))); VaultData memory _vaultData = vaultData[_collateralToken]; - require(_vaultData.vaultAddr != address(0), "err-invalid-request"); + if (_vaultData.vault == address(0)) revert InvalidRequest(); _euroDeposit(_collateralToken, _euroPercentage, _vaultData.pathToEURA); _otherDeposit(_collateralToken, _vaultData); - return (euroVault, _vaultData.vaultAddr); + return (euroVault, _vaultData.vault); + // TODO emit event } function _sellEUROs() private { @@ -166,21 +189,30 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { IERC20(EUROs).safeApprove(uniswapRouter, 0); } - function withdrawEUROsDeposit(address _token) private { - IHypervisor(euroVault).withdraw(_thisBalanceOf(euroVault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); + function _withdrawEUROsDeposit(address _vault, address _token) private { + IHypervisor(_vault).withdraw(_thisBalanceOf(_vault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); _sellEUROs(); _sellEURA(_token); } + function _withdrawOtherDeposit(address _vault, address _token) private { + VaultData memory _vaultData = vaultData[_token]; + if (_vaultData.vault != _vault) revert InvalidRequest(); + IHypervisor(_vault).withdraw(_thisBalanceOf(_vault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); + _swapToSingleAsset(_vault, _token, uniswapRouter, _vaultData.poolFee); + IERC20(_token).safeTransfer(msg.sender, _thisBalanceOf(_token)); + } + function withdraw(address _vault, address _token) external { IERC20(_vault).safeTransferFrom(msg.sender, address(this), IERC20(_vault).balanceOf(msg.sender)); - if (_vault == euroVault) { - withdrawEUROsDeposit(_token); - } + _vault == euroVault ? + _withdrawEUROsDeposit(_vault, _token) : + _withdrawOtherDeposit(_vault, _token); + // TODO emit event } - function addVaultData(address _collateralToken, address _vaultAddr, uint24 _poolFee, bytes memory _pathToEURA, bytes memory _pathFromEURA) external { - vaultData[_collateralToken] = VaultData(_vaultAddr, _poolFee, _pathToEURA, _pathFromEURA); + function addVaultData(address _collateralToken, address _vault, uint24 _poolFee, bytes memory _pathToEURA, bytes memory _pathFromEURA) external { + vaultData[_collateralToken] = VaultData(_vault, _poolFee, _pathToEURA, _pathFromEURA); } function removeVaultData(address _collateralToken) external onlyOwner { diff --git a/test/SmartVault.js b/test/SmartVault.js index 7991731..0455a50 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -468,7 +468,7 @@ describe('SmartVault', async () => { }); describe('yield', async () => { - let WBTC, WBTCPerETH, WETHGammaVaultMock; + let WBTC, WBTCPerETH, MockWETHWBTCVault; beforeEach(async () => { WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); @@ -478,18 +478,18 @@ describe('SmartVault', async () => { await TokenManager.addAcceptedToken(MockWeth.address, ClEthUsd.address); // fake gamma vault for WETH + WBTC - WETHGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( + MockWETHWBTCVault = await (await ethers.getContractFactory('GammaVaultMock')).deploy( 'WETH-WBTC', 'WETH-WBTC', MockWeth.address, WBTC.address ); // data about how yield manager converts collateral to EURA, vault addresses etc await YieldManager.addVaultData( - MockWeth.address, WETHGammaVaultMock.address, 500, + MockWeth.address, MockWETHWBTCVault.address, 500, new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [MockWeth.address, 3000, EURA.address]), new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, MockWeth.address]) ) await YieldManager.addVaultData( - WBTC.address, WETHGammaVaultMock.address, 500, + WBTC.address, MockWETHWBTCVault.address, 500, new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [WBTC.address, 3000, EURA.address]), new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, WBTC.address]) ) @@ -498,9 +498,9 @@ describe('SmartVault', async () => { await UniProxyMock.setRatio(EUROsGammaVaultMock.address, EURA.address, ethers.utils.parseEther('1')); // ratio of weth / wbtc vault is 1:1 in value, or 20:1 in unscaled numbers (20*10**10:1) in scaled WBTCPerETH = ethers.utils.parseUnits('0.05',8) - await UniProxyMock.setRatio(WETHGammaVaultMock.address, MockWeth.address, WBTCPerETH); + await UniProxyMock.setRatio(MockWETHWBTCVault.address, MockWeth.address, WBTCPerETH); // ratio is inverse of above, 1:20 in unscaled numbers, or 1:20*10^8 - await UniProxyMock.setRatio(WETHGammaVaultMock.address, WBTC.address, ethers.utils.parseUnits('20',28)); + await UniProxyMock.setRatio(MockWETHWBTCVault.address, WBTC.address, ethers.utils.parseUnits('20',28)); // set fake rate for swap router: this is ETH / EUROs: ~1500 await MockSwapRouter.setRate(MockWeth.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(ethers.utils.parseEther('1')).div(DEFAULT_EUR_USD_PRICE)); @@ -581,7 +581,7 @@ describe('SmartVault', async () => { await expect(YieldManager.connect(admin).removeVaultData(MockWeth.address)).not.to.be.reverted; await user.sendTransaction({ to: Vault.address, value: ethCollateral }); - await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWith('err-invalid-request'); + await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWithCustomError(YieldManager, 'InvalidRequest'); }); it('withdraw yield deposits by vault', async () => { @@ -597,14 +597,23 @@ describe('SmartVault', async () => { const [ EUROsYield ] = await Vault.yieldAssets(); await Vault.connect(user).withdrawYield(EUROsYield.vault, ETH); - const { totalCollateralValue, collateral } = await Vault.status(); + let { totalCollateralValue, collateral } = await Vault.status(); // fake rate from swap router causing a slight accuracy area expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 2000); const yieldAssets = await Vault.yieldAssets(); expect(yieldAssets).to.have.length(1); - expect(yieldAssets[0].vault).to.equal(WETHGammaVaultMock.address); + expect(yieldAssets[0].vault).to.equal(MockWETHWBTCVault.address); // should have withdrawn ~quarter of eth collateral, because that much was put in stable pool originally expect(getCollateralOf('ETH', collateral).amount).to.be.closeTo(ethCollateral.div(4), 1); + + await Vault.connect(user).withdrawYield(MockWETHWBTCVault.address, ethers.utils.formatBytes32String('WBTC')); + ({ totalCollateralValue, collateral } = await Vault.status()); + // fake rate from swap router causing a slight accuracy area + expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 2000); + expect(await Vault.yieldAssets()).to.be.empty; + // wbtc amount should be roughly equal to 0.075 ETH = 0.075 + const expectedWBTC = WBTCPerETH.mul(ethCollateral.mul(3).div(4)).div(ethers.utils.parseEther('1')); + expect(getCollateralOf('WBTC', collateral).amount).to.equal(expectedWBTC); }); xit('reverts if collateral level falls below required level'); From d59e5c3df44d70f890e457517f81c0581384537b Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 16:51:11 +0200 Subject: [PATCH 21/33] refactor swapping from euros to eura --- contracts/SmartVaultYieldManager.sol | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index b2bc98f..8af4e83 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -159,22 +159,6 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { // TODO emit event } - function _sellEUROs() private { - uint256 _balance = _thisBalanceOf(EUROs); - IERC20(EUROs).safeApprove(eurosRouter, _balance); - ISwapRouter(eurosRouter).exactInputSingle(ISwapRouter.ExactInputSingleParams({ - tokenIn: EUROs, - tokenOut: EURA, - fee: 500, - recipient: address(this), - deadline: block.timestamp + 60, - amountIn: _balance, - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - })); - IERC20(EUROs).safeApprove(eurosRouter, 0); - } - function _sellEURA(address _token) private { bytes memory _pathFromEURA = vaultData[_token].pathFromEURA; uint256 _balance = _thisBalanceOf(EURA); @@ -191,7 +175,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { function _withdrawEUROsDeposit(address _vault, address _token) private { IHypervisor(_vault).withdraw(_thisBalanceOf(_vault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); - _sellEUROs(); + _swapToSingleAsset(euroVault, EURA, eurosRouter, 500); _sellEURA(_token); } From df31d00ddcfdb6c596cd2ce04ab5f0bbae75cb4e Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 17:22:31 +0200 Subject: [PATCH 22/33] rename gamma vaults to hypervisors --- contracts/SmartVaultV4.sol | 62 ++++++------- contracts/SmartVaultYieldManager.sol | 93 ++++++++++--------- .../{GammaVaultMock.sol => Hypervisor.sol} | 2 +- newABI.json | 0 test/SmartVault.js | 34 +++---- 5 files changed, 96 insertions(+), 95 deletions(-) rename contracts/test_utils/{GammaVaultMock.sol => Hypervisor.sol} (97%) create mode 100644 newABI.json diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index 1e37852..008166d 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -24,7 +24,7 @@ contract SmartVaultV4 is ISmartVault { address public immutable manager; IEUROs public immutable EUROs; IPriceCalculator public immutable calculator; - address[] private vaultTokens; + address[] private Hypervisors; address public owner; uint256 private minted; @@ -35,7 +35,7 @@ contract SmartVaultV4 is ISmartVault { event EUROsMinted(address to, uint256 amount, uint256 fee); event EUROsBurned(uint256 amount, uint256 fee); - struct YieldPair { address vault; address token0; uint256 amount0; address token1; uint256 amount1; } + struct YieldPair { address hypervisor; address token0; uint256 amount0; address token1; uint256 amount1; } error InvalidUser(); error InvalidRequest(); @@ -72,14 +72,14 @@ contract SmartVaultV4 is ISmartVault { } function yieldVaultCollateral(ITokenManager.Token[] memory _acceptedTokens) private view returns (uint256 _euros) { - for (uint256 i = 0; i < vaultTokens.length; i++) { - IHypervisor _vaultToken = IHypervisor(vaultTokens[i]); - uint256 _balance = _vaultToken.balanceOf(address(this)); + for (uint256 i = 0; i < Hypervisors.length; i++) { + IHypervisor _Hypervisor = IHypervisor(Hypervisors[i]); + uint256 _balance = _Hypervisor.balanceOf(address(this)); if (_balance > 0) { - uint256 _totalSupply = _vaultToken.totalSupply(); - (uint256 _underlyingTotal0, uint256 _underlyingTotal1) = _vaultToken.getTotalAmounts(); - address _token0 = _vaultToken.token0(); - address _token1 = _vaultToken.token1(); + uint256 _totalSupply = _Hypervisor.totalSupply(); + (uint256 _underlyingTotal0, uint256 _underlyingTotal1) = _Hypervisor.getTotalAmounts(); + address _token0 = _Hypervisor.token0(); + address _token1 = _Hypervisor.token1(); uint256 _underlying0 = _balance * _underlyingTotal0 / _totalSupply; uint256 _underlying1 = _balance * _underlyingTotal1 / _totalSupply; if (_token0 == address(EUROs) || _token1 == address(EUROs)) { @@ -273,18 +273,18 @@ contract SmartVaultV4 is ISmartVault { executeERC20SwapAndFee(params, swapFee); } - function addUniqueVaultToken(address _vault) private { - for (uint256 i = 0; i < vaultTokens.length; i++) { - if (vaultTokens[i] == _vault) return; + function addUniqueHypervisor(address _vault) private { + for (uint256 i = 0; i < Hypervisors.length; i++) { + if (Hypervisors[i] == _vault) return; } - vaultTokens.push(_vault); + Hypervisors.push(_vault); } - function removeVaultToken(address _vault) private { - for (uint256 i = 0; i < vaultTokens.length; i++) { - if (vaultTokens[i] == _vault) { - vaultTokens[i] = vaultTokens[vaultTokens.length - 1]; - vaultTokens.pop(); + function removeHypervisor(address _vault) private { + for (uint256 i = 0; i < Hypervisors.length; i++) { + if (Hypervisors[i] == _vault) { + Hypervisors[i] = Hypervisors[Hypervisors.length - 1]; + Hypervisors.pop(); } } } @@ -296,31 +296,31 @@ contract SmartVaultV4 is ISmartVault { if (_balance == 0) revert InvalidRequest(); IERC20(_token).safeApprove(ISmartVaultManagerV3(manager).yieldManager(), _balance); (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).deposit(_token, _euroPercentage); - addUniqueVaultToken(_vault1); - addUniqueVaultToken(_vault2); + addUniqueHypervisor(_vault1); + addUniqueHypervisor(_vault2); } function withdrawYield(address _vault, bytes32 _symbol) external onlyOwner { address _token = getTokenisedAddr(_symbol); IERC20(_vault).safeApprove(ISmartVaultManagerV3(manager).yieldManager(), IERC20(_vault).balanceOf(address(this))); ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).withdraw(_vault, _token); - removeVaultToken(_vault); + removeHypervisor(_vault); if (_symbol == NATIVE) { IWETH(_token).withdraw(getAssetBalance(_token)); } } function yieldAssets() external view returns (YieldPair[] memory _yieldPairs) { - _yieldPairs = new YieldPair[](vaultTokens.length); - for (uint256 i = 0; i < vaultTokens.length; i++) { - IHypervisor _vaultToken = IHypervisor(vaultTokens[i]); - uint256 _balance = _vaultToken.balanceOf(address(this)); - uint256 _vaultTotal = _vaultToken.totalSupply(); - (uint256 _underlyingTotal0, uint256 _underlyingTotal1) = _vaultToken.getTotalAmounts(); - - _yieldPairs[i].vault = vaultTokens[i]; - _yieldPairs[i].token0 = _vaultToken.token0(); - _yieldPairs[i].token1 = _vaultToken.token1(); + _yieldPairs = new YieldPair[](Hypervisors.length); + for (uint256 i = 0; i < Hypervisors.length; i++) { + IHypervisor _Hypervisor = IHypervisor(Hypervisors[i]); + uint256 _balance = _Hypervisor.balanceOf(address(this)); + uint256 _vaultTotal = _Hypervisor.totalSupply(); + (uint256 _underlyingTotal0, uint256 _underlyingTotal1) = _Hypervisor.getTotalAmounts(); + + _yieldPairs[i].hypervisor = Hypervisors[i]; + _yieldPairs[i].token0 = _Hypervisor.token0(); + _yieldPairs[i].token1 = _Hypervisor.token1(); _yieldPairs[i].amount0 = _balance * _underlyingTotal0 / _vaultTotal; _yieldPairs[i].amount1 = _balance * _underlyingTotal1 / _vaultTotal; } diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index 8af4e83..6a5987d 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -19,22 +19,22 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { address private immutable WETH; address private immutable uniProxy; address private immutable eurosRouter; - address private immutable euroVault; + address private immutable euroHypervisor; address private immutable uniswapRouter; uint256 private constant HUNDRED_PC = 1e5; - mapping(address => VaultData) private vaultData; + mapping(address => HypervisorData) private hypervisorData; - struct VaultData { address vault; uint24 poolFee; bytes pathToEURA; bytes pathFromEURA; } + struct HypervisorData { address hypervisor; uint24 poolFee; bytes pathToEURA; bytes pathFromEURA; } error InvalidRequest(); - constructor(address _EUROs, address _EURA, address _WETH, address _uniProxy, address _eurosRouter, address _euroVault, address _uniswapRouter) { + constructor(address _EUROs, address _EURA, address _WETH, address _uniProxy, address _eurosRouter, address _euroHypervisor, address _uniswapRouter) { EUROs = _EUROs; EURA = _EURA; WETH = _WETH; uniProxy = _uniProxy; eurosRouter = _eurosRouter; - euroVault = _euroVault; + euroHypervisor = _euroHypervisor; uniswapRouter = _uniswapRouter; } @@ -95,10 +95,10 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { } } - function _swapToSingleAsset(address _vault, address _wantedToken, address _swapRouter, uint24 _fee) private { - address _token0 = IHypervisor(_vault).token0(); - address _unwantedToken = IHypervisor(_vault).token0() == _wantedToken ? - IHypervisor(_vault).token1() : + function _swapToSingleAsset(address _hypervisor, address _wantedToken, address _swapRouter, uint24 _fee) private { + address _token0 = IHypervisor(_hypervisor).token0(); + address _unwantedToken = IHypervisor(_hypervisor).token0() == _wantedToken ? + IHypervisor(_hypervisor).token1() : _token0; uint256 _balance = _thisBalanceOf(_unwantedToken); IERC20(_unwantedToken).safeApprove(_swapRouter, _balance); @@ -128,39 +128,40 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { IERC20(_collateralToken).safeApprove(uniswapRouter, 0); } - function _deposit(address _vault) private { - address _token0 = IHypervisor(_vault).token0(); - address _token1 = IHypervisor(_vault).token1(); - IERC20(_token0).safeApprove(_vault, _thisBalanceOf(_token0)); - IERC20(_token1).safeApprove(_vault, _thisBalanceOf(_token1)); - IUniProxy(uniProxy).deposit(_thisBalanceOf(_token0), _thisBalanceOf(_token1), msg.sender, _vault, [uint256(0),uint256(0),uint256(0),uint256(0)]); - IERC20(_token0).safeApprove(_vault, 0); - IERC20(_token1).safeApprove(_vault, 0); + function _deposit(address _hypervisor) private { + address _token0 = IHypervisor(_hypervisor).token0(); + address _token1 = IHypervisor(_hypervisor).token1(); + IERC20(_token0).safeApprove(_hypervisor, _thisBalanceOf(_token0)); + IERC20(_token1).safeApprove(_hypervisor, _thisBalanceOf(_token1)); + IUniProxy(uniProxy).deposit(_thisBalanceOf(_token0), _thisBalanceOf(_token1), msg.sender, _hypervisor, [uint256(0),uint256(0),uint256(0),uint256(0)]); + IERC20(_token0).safeApprove(_hypervisor, 0); + IERC20(_token1).safeApprove(_hypervisor, 0); } function _euroDeposit(address _collateralToken, uint256 _euroPercentage, bytes memory _pathToEURA) private { _swapToEURA(_collateralToken, _euroPercentage, _pathToEURA); - _swapToRatio(EURA, euroVault, eurosRouter, 500); - _deposit(euroVault); + _swapToRatio(EURA, euroHypervisor, eurosRouter, 500); + _deposit(euroHypervisor); } - function _otherDeposit(address _collateralToken, VaultData memory _vaultData) private { - _swapToRatio(_collateralToken, _vaultData.vault, uniswapRouter, _vaultData.poolFee); - _deposit(_vaultData.vault); + function _otherDeposit(address _collateralToken, HypervisorData memory _hypervisorData) private { + _swapToRatio(_collateralToken, _hypervisorData.hypervisor, uniswapRouter, _hypervisorData.poolFee); + _deposit(_hypervisorData.hypervisor); } - function deposit(address _collateralToken, uint256 _euroPercentage) external returns (address _vault0, address _vault1) { + function deposit(address _collateralToken, uint256 _euroPercentage) external returns (address _hypervisor0, address _hypervisor1) { + // TODO min _euroPercentage of 10% IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), IERC20(_collateralToken).balanceOf(address(msg.sender))); - VaultData memory _vaultData = vaultData[_collateralToken]; - if (_vaultData.vault == address(0)) revert InvalidRequest(); - _euroDeposit(_collateralToken, _euroPercentage, _vaultData.pathToEURA); - _otherDeposit(_collateralToken, _vaultData); - return (euroVault, _vaultData.vault); + HypervisorData memory _hypervisorData = hypervisorData[_collateralToken]; + if (_hypervisorData.hypervisor == address(0)) revert InvalidRequest(); + _euroDeposit(_collateralToken, _euroPercentage, _hypervisorData.pathToEURA); + _otherDeposit(_collateralToken, _hypervisorData); + return (euroHypervisor, _hypervisorData.hypervisor); // TODO emit event } function _sellEURA(address _token) private { - bytes memory _pathFromEURA = vaultData[_token].pathFromEURA; + bytes memory _pathFromEURA = hypervisorData[_token].pathFromEURA; uint256 _balance = _thisBalanceOf(EURA); IERC20(EURA).safeApprove(uniswapRouter, _balance); ISwapRouter(uniswapRouter).exactInput(ISwapRouter.ExactInputParams({ @@ -173,33 +174,33 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { IERC20(EUROs).safeApprove(uniswapRouter, 0); } - function _withdrawEUROsDeposit(address _vault, address _token) private { - IHypervisor(_vault).withdraw(_thisBalanceOf(_vault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); - _swapToSingleAsset(euroVault, EURA, eurosRouter, 500); + function _withdrawEUROsDeposit(address _hypervisor, address _token) private { + IHypervisor(_hypervisor).withdraw(_thisBalanceOf(_hypervisor), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); + _swapToSingleAsset(euroHypervisor, EURA, eurosRouter, 500); _sellEURA(_token); } - function _withdrawOtherDeposit(address _vault, address _token) private { - VaultData memory _vaultData = vaultData[_token]; - if (_vaultData.vault != _vault) revert InvalidRequest(); - IHypervisor(_vault).withdraw(_thisBalanceOf(_vault), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); - _swapToSingleAsset(_vault, _token, uniswapRouter, _vaultData.poolFee); + function _withdrawOtherDeposit(address _hypervisor, address _token) private { + HypervisorData memory _hypervisorData = hypervisorData[_token]; + if (_hypervisorData.hypervisor != _hypervisor) revert InvalidRequest(); + IHypervisor(_hypervisor).withdraw(_thisBalanceOf(_hypervisor), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); + _swapToSingleAsset(_hypervisor, _token, uniswapRouter, _hypervisorData.poolFee); IERC20(_token).safeTransfer(msg.sender, _thisBalanceOf(_token)); } - function withdraw(address _vault, address _token) external { - IERC20(_vault).safeTransferFrom(msg.sender, address(this), IERC20(_vault).balanceOf(msg.sender)); - _vault == euroVault ? - _withdrawEUROsDeposit(_vault, _token) : - _withdrawOtherDeposit(_vault, _token); + function withdraw(address _hypervisor, address _token) external { + IERC20(_hypervisor).safeTransferFrom(msg.sender, address(this), IERC20(_hypervisor).balanceOf(msg.sender)); + _hypervisor == euroHypervisor ? + _withdrawEUROsDeposit(_hypervisor, _token) : + _withdrawOtherDeposit(_hypervisor, _token); // TODO emit event } - function addVaultData(address _collateralToken, address _vault, uint24 _poolFee, bytes memory _pathToEURA, bytes memory _pathFromEURA) external { - vaultData[_collateralToken] = VaultData(_vault, _poolFee, _pathToEURA, _pathFromEURA); + function addHypervisorData(address _collateralToken, address _hypervisor, uint24 _poolFee, bytes memory _pathToEURA, bytes memory _pathFromEURA) external { + hypervisorData[_collateralToken] = HypervisorData(_hypervisor, _poolFee, _pathToEURA, _pathFromEURA); } - function removeVaultData(address _collateralToken) external onlyOwner { - delete vaultData[_collateralToken]; + function removeHypervisorData(address _collateralToken) external onlyOwner { + delete hypervisorData[_collateralToken]; } } diff --git a/contracts/test_utils/GammaVaultMock.sol b/contracts/test_utils/Hypervisor.sol similarity index 97% rename from contracts/test_utils/GammaVaultMock.sol rename to contracts/test_utils/Hypervisor.sol index 2b4a297..066ebe4 100644 --- a/contracts/test_utils/GammaVaultMock.sol +++ b/contracts/test_utils/Hypervisor.sol @@ -7,7 +7,7 @@ import "contracts/interfaces/IHypervisor.sol"; import "hardhat/console.sol"; -contract GammaVaultMock is IHypervisor, ERC20 { +contract HypervisorMock is IHypervisor, ERC20 { address public immutable token0; address public immutable token1; diff --git a/newABI.json b/newABI.json new file mode 100644 index 0000000..e69de29 diff --git a/test/SmartVault.js b/test/SmartVault.js index 0455a50..2fcf85e 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -4,7 +4,7 @@ const { expect } = require('chai'); const { DEFAULT_ETH_USD_PRICE, DEFAULT_EUR_USD_PRICE, DEFAULT_COLLATERAL_RATE, PROTOCOL_FEE_RATE, getCollateralOf, ETH, getNFTMetadataContract, fullyUpgradedSmartVaultManager, TEST_VAULT_LIMIT } = require('./common'); const { HUNDRED_PC } = require('./common'); -let VaultManager, Vault, TokenManager, ClEthUsd, EUROs, EURA, MockSwapRouter, MockWeth, admin, user, otherUser, protocol, YieldManager, UniProxyMock, EUROsGammaVaultMock; +let VaultManager, Vault, TokenManager, ClEthUsd, EUROs, EURA, MockSwapRouter, MockWeth, admin, user, otherUser, protocol, YieldManager, UniProxyMock, MockEUROsHypervisor; describe('SmartVault', async () => { beforeEach(async () => { @@ -22,11 +22,11 @@ describe('SmartVault', async () => { MockWeth = await (await ethers.getContractFactory('MockWETH')).deploy(); EURA = await (await ethers.getContractFactory('ERC20Mock')).deploy('EURA', 'EURA', 18); UniProxyMock = await (await ethers.getContractFactory('UniProxyMock')).deploy(); - EUROsGammaVaultMock = await (await ethers.getContractFactory('GammaVaultMock')).deploy( + MockEUROsHypervisor = await (await ethers.getContractFactory('HypervisorMock')).deploy( 'EUROs-EURA', 'EUROs-EURA', EUROs.address, EURA.address ); YieldManager = await (await ethers.getContractFactory('SmartVaultYieldManager')).deploy( - EUROs.address, EURA.address, MockWeth.address, UniProxyMock.address, MockSwapRouter.address, EUROsGammaVaultMock.address, + EUROs.address, EURA.address, MockWeth.address, UniProxyMock.address, MockSwapRouter.address, MockEUROsHypervisor.address, MockSwapRouter.address ); VaultManager = await fullyUpgradedSmartVaultManager( @@ -468,7 +468,7 @@ describe('SmartVault', async () => { }); describe('yield', async () => { - let WBTC, WBTCPerETH, MockWETHWBTCVault; + let WBTC, WBTCPerETH, MockWETHWBTCHypervisor; beforeEach(async () => { WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); @@ -478,29 +478,29 @@ describe('SmartVault', async () => { await TokenManager.addAcceptedToken(MockWeth.address, ClEthUsd.address); // fake gamma vault for WETH + WBTC - MockWETHWBTCVault = await (await ethers.getContractFactory('GammaVaultMock')).deploy( + MockWETHWBTCHypervisor = await (await ethers.getContractFactory('HypervisorMock')).deploy( 'WETH-WBTC', 'WETH-WBTC', MockWeth.address, WBTC.address ); // data about how yield manager converts collateral to EURA, vault addresses etc - await YieldManager.addVaultData( - MockWeth.address, MockWETHWBTCVault.address, 500, + await YieldManager.addHypervisorData( + MockWeth.address, MockWETHWBTCHypervisor.address, 500, new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [MockWeth.address, 3000, EURA.address]), new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, MockWeth.address]) ) - await YieldManager.addVaultData( - WBTC.address, MockWETHWBTCVault.address, 500, + await YieldManager.addHypervisorData( + WBTC.address, MockWETHWBTCHypervisor.address, 500, new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [WBTC.address, 3000, EURA.address]), new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, WBTC.address]) ) // ratio of euros vault is 1:1 - await UniProxyMock.setRatio(EUROsGammaVaultMock.address, EURA.address, ethers.utils.parseEther('1')); + await UniProxyMock.setRatio(MockEUROsHypervisor.address, EURA.address, ethers.utils.parseEther('1')); // ratio of weth / wbtc vault is 1:1 in value, or 20:1 in unscaled numbers (20*10**10:1) in scaled WBTCPerETH = ethers.utils.parseUnits('0.05',8) - await UniProxyMock.setRatio(MockWETHWBTCVault.address, MockWeth.address, WBTCPerETH); + await UniProxyMock.setRatio(MockWETHWBTCHypervisor.address, MockWeth.address, WBTCPerETH); // ratio is inverse of above, 1:20 in unscaled numbers, or 1:20*10^8 - await UniProxyMock.setRatio(MockWETHWBTCVault.address, WBTC.address, ethers.utils.parseUnits('20',28)); + await UniProxyMock.setRatio(MockWETHWBTCHypervisor.address, WBTC.address, ethers.utils.parseUnits('20',28)); // set fake rate for swap router: this is ETH / EUROs: ~1500 await MockSwapRouter.setRate(MockWeth.address, EURA.address, DEFAULT_ETH_USD_PRICE.mul(ethers.utils.parseEther('1')).div(DEFAULT_EUR_USD_PRICE)); @@ -577,8 +577,8 @@ describe('SmartVault', async () => { await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).not.to.be.reverted; - await expect(YieldManager.connect(user).removeVaultData(MockWeth.address)).to.be.revertedWith('Ownable: caller is not the owner'); - await expect(YieldManager.connect(admin).removeVaultData(MockWeth.address)).not.to.be.reverted; + await expect(YieldManager.connect(user).removeHypervisorData(MockWeth.address)).to.be.revertedWith('Ownable: caller is not the owner'); + await expect(YieldManager.connect(admin).removeHypervisorData(MockWeth.address)).not.to.be.reverted; await user.sendTransaction({ to: Vault.address, value: ethCollateral }); await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWithCustomError(YieldManager, 'InvalidRequest'); @@ -596,17 +596,17 @@ describe('SmartVault', async () => { expect(getCollateralOf('ETH', status.collateral).amount).to.equal(0); const [ EUROsYield ] = await Vault.yieldAssets(); - await Vault.connect(user).withdrawYield(EUROsYield.vault, ETH); + await Vault.connect(user).withdrawYield(EUROsYield.hypervisor, ETH); let { totalCollateralValue, collateral } = await Vault.status(); // fake rate from swap router causing a slight accuracy area expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 2000); const yieldAssets = await Vault.yieldAssets(); expect(yieldAssets).to.have.length(1); - expect(yieldAssets[0].vault).to.equal(MockWETHWBTCVault.address); + expect(yieldAssets[0].hypervisor).to.equal(MockWETHWBTCHypervisor.address); // should have withdrawn ~quarter of eth collateral, because that much was put in stable pool originally expect(getCollateralOf('ETH', collateral).amount).to.be.closeTo(ethCollateral.div(4), 1); - await Vault.connect(user).withdrawYield(MockWETHWBTCVault.address, ethers.utils.formatBytes32String('WBTC')); + await Vault.connect(user).withdrawYield(MockWETHWBTCHypervisor.address, ethers.utils.formatBytes32String('WBTC')); ({ totalCollateralValue, collateral } = await Vault.status()); // fake rate from swap router causing a slight accuracy area expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 2000); From 6723325180dc79e9822e0585b3b6076516bee41f Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Mon, 19 Aug 2024 17:57:24 +0200 Subject: [PATCH 23/33] remove junk abi file --- newABI.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 newABI.json diff --git a/newABI.json b/newABI.json deleted file mode 100644 index e69de29..0000000 From dc9ef04dc1154e7fb1a515a15743c8d769d1ea43 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 20 Aug 2024 11:13:52 +0200 Subject: [PATCH 24/33] test reversions for invalid yield deposit + withdrawal requests --- test/SmartVault.js | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/test/SmartVault.js b/test/SmartVault.js index 2fcf85e..daf4590 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -467,15 +467,19 @@ describe('SmartVault', async () => { }); }); - describe('yield', async () => { - let WBTC, WBTCPerETH, MockWETHWBTCHypervisor; + describe.only('yield', async () => { + let WBTC, USDC, WBTCPerETH, MockWETHWBTCHypervisor; beforeEach(async () => { WBTC = await (await ethers.getContractFactory('ERC20Mock')).deploy('Wrapped Bitcoin', 'WBTC', 8); + USDC = await (await ethers.getContractFactory('ERC20Mock')).deploy('USD Coin', 'USDC', 6); const CL_WBTC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('WBTC / USD'); await CL_WBTC_USD.setPrice(DEFAULT_ETH_USD_PRICE.mul(20)); + const CL_USDC_USD = await (await ethers.getContractFactory('ChainlinkMock')).deploy('USDC / USD'); + await CL_USDC_USD.setPrice(BigNumber.from(10).pow(8)); await TokenManager.addAcceptedToken(WBTC.address, CL_WBTC_USD.address); await TokenManager.addAcceptedToken(MockWeth.address, ClEthUsd.address); + await TokenManager.addAcceptedToken(USDC.address, CL_USDC_USD.address); // fake gamma vault for WETH + WBTC MockWETHWBTCHypervisor = await (await ethers.getContractFactory('HypervisorMock')).deploy( @@ -538,6 +542,13 @@ describe('SmartVault', async () => { await expect(Vault.connect(admin).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWithCustomError(Vault, 'InvalidUser'); await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).not.to.be.reverted; + // USDC does not have hypervisor data set in yield manager + const USDCBytes = ethers.utils.formatBytes32String('USDC'); + const USDCCollateral = ethers.utils.parseUnits('1000', 6); + await USDC.mint(Vault.address, USDCCollateral); + await expect(Vault.connect(user).depositYield(USDCBytes, HUNDRED_PC.div(2))).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); + await Vault.connect(user).removeCollateral(USDCBytes, USDCCollateral, user.address); + ({ collateral, totalCollateralValue } = await Vault.status()); expect(getCollateralOf('ETH', collateral).amount).to.equal(0); // allow a delta of 2 wei in pre and post yield collateral, due to dividing etc @@ -616,7 +627,26 @@ describe('SmartVault', async () => { expect(getCollateralOf('WBTC', collateral).amount).to.equal(expectedWBTC); }); + it('reverts when collateral asset is not compatible with given asset on withdrawal', async () => { + const ethCollateral = ethers.utils.parseEther('0.1'); + await user.sendTransaction({ to: Vault.address, value: ethCollateral }); + await Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2)); + + // add usdc hypervisor data + const MockUSDCWETHHypervisor = await (await ethers.getContractFactory('HypervisorMock')).deploy( + 'USDC-WETH', 'USDC-WETH', USDC.address, MockWeth.address + ); + await YieldManager.addHypervisorData( + USDC.address, MockUSDCWETHHypervisor.address, 500, + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [USDC.address, 3000, EURA.address]), + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, USDC.address]) + ) + + // weth / wbtc hypervisor cannot be withdrawn to usdc, even tho there is usdc hypervisor data + await expect(Vault.connect(user).withdrawYield(MockWETHWBTCHypervisor.address, ethers.utils.formatBytes32String('USDC'))) + .to.be.revertedWithCustomError(YieldManager, 'InvalidRequest'); + }) + xit('reverts if collateral level falls below required level'); - xit('reverts if withdrawal asset is not compatible with yield vault'); }); }); \ No newline at end of file From a379f93a9fc55fa33995fdf3fc5418b612f8b8c9 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 20 Aug 2024 11:14:54 +0200 Subject: [PATCH 25/33] test reversions for invalid yield deposit + withdrawal requests --- test/SmartVault.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/SmartVault.js b/test/SmartVault.js index daf4590..896409b 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -467,7 +467,7 @@ describe('SmartVault', async () => { }); }); - describe.only('yield', async () => { + describe('yield', async () => { let WBTC, USDC, WBTCPerETH, MockWETHWBTCHypervisor; beforeEach(async () => { From ece1801e4eb0dbbc8a1f04923add93e6383ce92e Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 20 Aug 2024 11:26:03 +0200 Subject: [PATCH 26/33] deposit reverts if vault becomes undercollateralised --- contracts/SmartVaultV4.sol | 1 + test/SmartVault.js | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index 008166d..f40fdac 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -296,6 +296,7 @@ contract SmartVaultV4 is ISmartVault { if (_balance == 0) revert InvalidRequest(); IERC20(_token).safeApprove(ISmartVaultManagerV3(manager).yieldManager(), _balance); (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).deposit(_token, _euroPercentage); + if (undercollateralised()) revert InvalidRequest(); addUniqueHypervisor(_vault1); addUniqueHypervisor(_vault2); } diff --git a/test/SmartVault.js b/test/SmartVault.js index 896409b..e514fc6 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -467,7 +467,7 @@ describe('SmartVault', async () => { }); }); - describe('yield', async () => { + describe.only('yield', async () => { let WBTC, USDC, WBTCPerETH, MockWETHWBTCHypervisor; beforeEach(async () => { @@ -647,6 +647,21 @@ describe('SmartVault', async () => { .to.be.revertedWithCustomError(YieldManager, 'InvalidRequest'); }) - xit('reverts if collateral level falls below required level'); + it('reverts if collateral level falls below required level during deposit or withdrawal', async () => { + const ethCollateral = ethers.utils.parseEther('0.1'); + await user.sendTransaction({ to: Vault.address, value: ethCollateral }); + // should be able to borrow up to about €125 + // borrowing 120 to allow for minting fee + await Vault.connect(user).mint(user.address, ethers.utils.parseEther('120')); + + // WBTC / ETH swap price tanks, so yield value is significantly lower than ETH collateral value + await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH.div(2)); + + await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); + + // // reset WBTC / ETH to normal rate + // await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH); + // await Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2)) + }); }); }); \ No newline at end of file From 6d563636c05754cbf219a967b2ef6fc3ffaf943b Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 20 Aug 2024 11:45:57 +0200 Subject: [PATCH 27/33] withdrawal reverts if collateral value drops below required --- contracts/SmartVaultV4.sol | 3 ++- test/SmartVault.js | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index f40fdac..615de48 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -296,9 +296,9 @@ contract SmartVaultV4 is ISmartVault { if (_balance == 0) revert InvalidRequest(); IERC20(_token).safeApprove(ISmartVaultManagerV3(manager).yieldManager(), _balance); (address _vault1, address _vault2) = ISmartVaultYieldManager(ISmartVaultManagerV3(manager).yieldManager()).deposit(_token, _euroPercentage); - if (undercollateralised()) revert InvalidRequest(); addUniqueHypervisor(_vault1); addUniqueHypervisor(_vault2); + if (undercollateralised()) revert InvalidRequest(); } function withdrawYield(address _vault, bytes32 _symbol) external onlyOwner { @@ -309,6 +309,7 @@ contract SmartVaultV4 is ISmartVault { if (_symbol == NATIVE) { IWETH(_token).withdraw(getAssetBalance(_token)); } + if (undercollateralised()) revert InvalidRequest(); } function yieldAssets() external view returns (YieldPair[] memory _yieldPairs) { diff --git a/test/SmartVault.js b/test/SmartVault.js index e514fc6..42d3e51 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -467,7 +467,7 @@ describe('SmartVault', async () => { }); }); - describe.only('yield', async () => { + describe('yield', async () => { let WBTC, USDC, WBTCPerETH, MockWETHWBTCHypervisor; beforeEach(async () => { @@ -654,14 +654,20 @@ describe('SmartVault', async () => { // borrowing 120 to allow for minting fee await Vault.connect(user).mint(user.address, ethers.utils.parseEther('120')); - // WBTC / ETH swap price tanks, so yield value is significantly lower than ETH collateral value + // ETH / WBTC swap price halves, so yield value is significantly lower than ETH collateral value await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH.div(2)); await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); - // // reset WBTC / ETH to normal rate - // await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH); - // await Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2)) + // reset ETH / WBTC to normal rate + await MockSwapRouter.setRate(MockWeth.address, WBTC.address, WBTCPerETH); + await Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2)); + + // WBTC / ETH swap price halves, so yield value is significantly lower than ETH collateral value + await MockSwapRouter.setRate(WBTC.address, MockWeth.address, ethers.utils.parseUnits('10',28)) + + await expect(Vault.connect(user).withdrawYield(MockWETHWBTCHypervisor.address, ETH)).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); + }); }); }); \ No newline at end of file From a55cde014b5311f7d79a57e07ac6b7e3b90c8759 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 20 Aug 2024 16:49:30 +0200 Subject: [PATCH 28/33] emit deposit + withdraw events --- contracts/SmartVaultYieldManager.sol | 14 +++++++++----- test/SmartVault.js | 13 +++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index 6a5987d..709f675 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -26,6 +26,8 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { struct HypervisorData { address hypervisor; uint24 poolFee; bytes pathToEURA; bytes pathFromEURA; } + event Deposit(address indexed smartVault, address indexed token, uint256 amount, uint256 euroPercentage); + event Withdraw(address indexed smartVault, address indexed token, address hypervisor, uint256 amount); error InvalidRequest(); constructor(address _EUROs, address _EURA, address _WETH, address _uniProxy, address _eurosRouter, address _euroHypervisor, address _uniswapRouter) { @@ -151,13 +153,14 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { function deposit(address _collateralToken, uint256 _euroPercentage) external returns (address _hypervisor0, address _hypervisor1) { // TODO min _euroPercentage of 10% - IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), IERC20(_collateralToken).balanceOf(address(msg.sender))); + uint256 _balance = IERC20(_collateralToken).balanceOf(address(msg.sender)); + IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), _balance); HypervisorData memory _hypervisorData = hypervisorData[_collateralToken]; if (_hypervisorData.hypervisor == address(0)) revert InvalidRequest(); _euroDeposit(_collateralToken, _euroPercentage, _hypervisorData.pathToEURA); _otherDeposit(_collateralToken, _hypervisorData); + emit Deposit(msg.sender, _collateralToken, _balance, _euroPercentage); return (euroHypervisor, _hypervisorData.hypervisor); - // TODO emit event } function _sellEURA(address _token) private { @@ -166,7 +169,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { IERC20(EURA).safeApprove(uniswapRouter, _balance); ISwapRouter(uniswapRouter).exactInput(ISwapRouter.ExactInputParams({ path: _pathFromEURA, - recipient: msg.sender, + recipient: address(this), deadline: block.timestamp + 60, amountIn: _balance, amountOutMinimum: 0 @@ -185,7 +188,6 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { if (_hypervisorData.hypervisor != _hypervisor) revert InvalidRequest(); IHypervisor(_hypervisor).withdraw(_thisBalanceOf(_hypervisor), address(this), address(this), [uint256(0),uint256(0),uint256(0),uint256(0)]); _swapToSingleAsset(_hypervisor, _token, uniswapRouter, _hypervisorData.poolFee); - IERC20(_token).safeTransfer(msg.sender, _thisBalanceOf(_token)); } function withdraw(address _hypervisor, address _token) external { @@ -193,7 +195,9 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { _hypervisor == euroHypervisor ? _withdrawEUROsDeposit(_hypervisor, _token) : _withdrawOtherDeposit(_hypervisor, _token); - // TODO emit event + uint256 _withdrawn = _thisBalanceOf(_token); + IERC20(_token).safeTransfer(msg.sender, _withdrawn); + emit Withdraw(msg.sender, _token, _hypervisor, _withdrawn); } function addHypervisorData(address _collateralToken, address _hypervisor, uint24 _poolFee, bytes memory _pathToEURA, bytes memory _pathFromEURA) external { diff --git a/test/SmartVault.js b/test/SmartVault.js index 42d3e51..c182bc5 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -539,8 +539,11 @@ describe('SmartVault', async () => { expect(getCollateralOf('ETH', collateral).amount).to.equal(ethCollateral); // only vault owner can deposit collateral as yield - await expect(Vault.connect(admin).depositYield(ETH, HUNDRED_PC.div(2))).to.be.revertedWithCustomError(Vault, 'InvalidUser'); - await expect(Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2))).not.to.be.reverted; + let depositYield = Vault.connect(admin).depositYield(ETH, HUNDRED_PC.div(2)); + await expect(depositYield).to.be.revertedWithCustomError(Vault, 'InvalidUser'); + depositYield = Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2)); + await expect(depositYield).not.to.be.reverted; + await expect(depositYield).to.emit(YieldManager, 'Deposit').withArgs(Vault.address, MockWeth.address, ethCollateral, HUNDRED_PC.div(2)); // USDC does not have hypervisor data set in yield manager const USDCBytes = ethers.utils.formatBytes32String('USDC'); @@ -575,7 +578,8 @@ describe('SmartVault', async () => { preYieldCollateral = totalCollateralValue; // deposit wbtc for yield, 25% to euros pool - await Vault.connect(user).depositYield(ethers.utils.formatBytes32String('WBTC'), HUNDRED_PC.div(4)); + depositYield = Vault.connect(user).depositYield(ethers.utils.formatBytes32String('WBTC'), HUNDRED_PC.div(4)); + await expect(depositYield).to.emit(YieldManager, 'Deposit').withArgs(Vault.address, WBTC.address, wbtcCollateral, HUNDRED_PC.div(4)); // bit of accuracy issue ({ collateral, totalCollateralValue } = await Vault.status()); expect(getCollateralOf('WBTC', collateral).amount).to.equal(0); expect(totalCollateralValue).to.be.closeTo(preYieldCollateral, 1); @@ -607,7 +611,8 @@ describe('SmartVault', async () => { expect(getCollateralOf('ETH', status.collateral).amount).to.equal(0); const [ EUROsYield ] = await Vault.yieldAssets(); - await Vault.connect(user).withdrawYield(EUROsYield.hypervisor, ETH); + let withdrawYield = Vault.connect(user).withdrawYield(EUROsYield.hypervisor, ETH); + await expect(withdrawYield).to.emit(YieldManager, 'Withdraw').withArgs(Vault.address, MockWeth.address, MockEUROsHypervisor.address, ethCollateral.div(4).sub(1)) // bit of an accuracy issue let { totalCollateralValue, collateral } = await Vault.status(); // fake rate from swap router causing a slight accuracy area expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 2000); From 058c97958e22f3daae32c4981e3efd92b3deb028 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 20 Aug 2024 17:01:11 +0200 Subject: [PATCH 29/33] remove outdated todo --- contracts/SmartVaultV4.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/SmartVaultV4.sol b/contracts/SmartVaultV4.sol index 615de48..cbdb6e9 100644 --- a/contracts/SmartVaultV4.sol +++ b/contracts/SmartVaultV4.sol @@ -87,8 +87,6 @@ contract SmartVaultV4 is ISmartVault { _euros += _underlying0; _euros += _underlying1; } else { - // TODO how do we deal with WETH as underlying token? - // add WETH as collateral? or check for it here? for (uint256 j = 0; j < _acceptedTokens.length; j++) { ITokenManager.Token memory _token = _acceptedTokens[j]; if (_token.addr == _token0) _euros += calculator.tokenToEur(_token, _underlying0); From a08ff12b9cad5fc5fed4e94ef83a795cbe74d362 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Tue, 20 Aug 2024 17:11:37 +0200 Subject: [PATCH 30/33] require min 10% to euros vault --- contracts/SmartVaultYieldManager.sol | 4 +++- test/SmartVault.js | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index 709f675..01d95d4 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -22,6 +22,8 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { address private immutable euroHypervisor; address private immutable uniswapRouter; uint256 private constant HUNDRED_PC = 1e5; + // min 10% to euros pool + uint256 private constant MIN_EURO_PERCENTAGE = 1e4; mapping(address => HypervisorData) private hypervisorData; struct HypervisorData { address hypervisor; uint24 poolFee; bytes pathToEURA; bytes pathFromEURA; } @@ -152,7 +154,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { } function deposit(address _collateralToken, uint256 _euroPercentage) external returns (address _hypervisor0, address _hypervisor1) { - // TODO min _euroPercentage of 10% + if (_euroPercentage < MIN_EURO_PERCENTAGE) revert InvalidRequest(); uint256 _balance = IERC20(_collateralToken).balanceOf(address(msg.sender)); IERC20(_collateralToken).safeTransferFrom(msg.sender, address(this), _balance); HypervisorData memory _hypervisorData = hypervisorData[_collateralToken]; diff --git a/test/SmartVault.js b/test/SmartVault.js index c182bc5..a413e66 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -541,6 +541,9 @@ describe('SmartVault', async () => { // only vault owner can deposit collateral as yield let depositYield = Vault.connect(admin).depositYield(ETH, HUNDRED_PC.div(2)); await expect(depositYield).to.be.revertedWithCustomError(Vault, 'InvalidUser'); + // 5% to stables pool is below minimum + depositYield = Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(20)); + await expect(depositYield).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); depositYield = Vault.connect(user).depositYield(ETH, HUNDRED_PC.div(2)); await expect(depositYield).not.to.be.reverted; await expect(depositYield).to.emit(YieldManager, 'Deposit').withArgs(Vault.address, MockWeth.address, ethCollateral, HUNDRED_PC.div(2)); From ba3e675804d81d25139bfce6cf54535da40eeb44 Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 21 Aug 2024 12:24:34 +0200 Subject: [PATCH 31/33] skims fee for protocol on yield withdrawal --- contracts/SmartVaultYieldManager.sol | 11 ++++++++++ test/SmartVault.js | 30 +++++++++++++++++++--------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index 01d95d4..f63e17b 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "contracts/interfaces/IHypervisor.sol"; import "contracts/interfaces/ISmartVaultYieldManager.sol"; +import "contracts/interfaces/ISmartVaultManager.sol"; import "contracts/interfaces/ISwapRouter.sol"; import "contracts/interfaces/IUniProxy.sol"; import "contracts/interfaces/IWETH.sol"; @@ -24,6 +25,8 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { uint256 private constant HUNDRED_PC = 1e5; // min 10% to euros pool uint256 private constant MIN_EURO_PERCENTAGE = 1e4; + address private smartVaultManager; + uint256 public feeRate; mapping(address => HypervisorData) private hypervisorData; struct HypervisorData { address hypervisor; uint24 poolFee; bytes pathToEURA; bytes pathFromEURA; } @@ -198,6 +201,9 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { _withdrawEUROsDeposit(_hypervisor, _token) : _withdrawOtherDeposit(_hypervisor, _token); uint256 _withdrawn = _thisBalanceOf(_token); + uint256 _fee = _withdrawn * feeRate / HUNDRED_PC; + _withdrawn = _withdrawn - _fee; + IERC20(_token).safeTransfer(ISmartVaultManager(smartVaultManager).protocol(), _fee); IERC20(_token).safeTransfer(msg.sender, _withdrawn); emit Withdraw(msg.sender, _token, _hypervisor, _withdrawn); } @@ -209,4 +215,9 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { function removeHypervisorData(address _collateralToken) external onlyOwner { delete hypervisorData[_collateralToken]; } + + function setFeeData(uint256 _feeRate, address _smartVaultManager) external { + feeRate = _feeRate; + smartVaultManager = _smartVaultManager; + } } diff --git a/test/SmartVault.js b/test/SmartVault.js index a413e66..0a368cb 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -35,6 +35,7 @@ describe('SmartVault', async () => { SmartVaultIndex.address, NFTMetadataGenerator.address, MockWeth.address, MockSwapRouter.address, TEST_VAULT_LIMIT, YieldManager.address ); + await YieldManager.setFeeData(PROTOCOL_FEE_RATE, VaultManager.address); await SmartVaultIndex.setVaultManager(VaultManager.address); await EUROs.grantRole(await EUROs.DEFAULT_ADMIN_ROLE(), VaultManager.address); await EUROs.grantRole(await EUROs.MINTER_ROLE(), admin.address); @@ -615,24 +616,30 @@ describe('SmartVault', async () => { const [ EUROsYield ] = await Vault.yieldAssets(); let withdrawYield = Vault.connect(user).withdrawYield(EUROsYield.hypervisor, ETH); - await expect(withdrawYield).to.emit(YieldManager, 'Withdraw').withArgs(Vault.address, MockWeth.address, MockEUROsHypervisor.address, ethCollateral.div(4).sub(1)) // bit of an accuracy issue + let protocolFee = ethCollateral.div(4).mul(PROTOCOL_FEE_RATE).div(HUNDRED_PC); + await expect(withdrawYield).to.emit(YieldManager, 'Withdraw').withArgs(Vault.address, MockWeth.address, MockEUROsHypervisor.address, ethCollateral.div(4).sub(protocolFee)) // bit of an accuracy issue let { totalCollateralValue, collateral } = await Vault.status(); - // fake rate from swap router causing a slight accuracy area - expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 2000); + // ~99.875% of collateral expected because of protocol fee on quarter of yield withdrawal + let expectedCollateral = preWithdrawCollateralValue.mul(99875).div(100000); + expect(totalCollateralValue).to.be.closeTo(expectedCollateral, 2000); const yieldAssets = await Vault.yieldAssets(); expect(yieldAssets).to.have.length(1); expect(yieldAssets[0].hypervisor).to.equal(MockWETHWBTCHypervisor.address); - // should have withdrawn ~quarter of eth collateral, because that much was put in stable pool originally - expect(getCollateralOf('ETH', collateral).amount).to.be.closeTo(ethCollateral.div(4), 1); - + // should have withdrawn ~quarter of eth collateral, because that much was put in stable pool originally, minus protocol fee + expect(getCollateralOf('ETH', collateral).amount).to.be.closeTo(ethCollateral.div(4).sub(protocolFee), 1); + expect(await MockWeth.balanceOf(protocol.address)).to.be.closeTo(protocolFee, 1); await Vault.connect(user).withdrawYield(MockWETHWBTCHypervisor.address, ethers.utils.formatBytes32String('WBTC')); ({ totalCollateralValue, collateral } = await Vault.status()); - // fake rate from swap router causing a slight accuracy area - expect(totalCollateralValue).to.be.closeTo(preWithdrawCollateralValue, 2000); + // ~99.5% of original collateral because all collateral withdrawn with .5% protocol fee rate + expectedCollateral = preWithdrawCollateralValue.mul(HUNDRED_PC.sub(PROTOCOL_FEE_RATE)).div(HUNDRED_PC); + expect(totalCollateralValue).to.be.closeTo(expectedCollateral, 2000); expect(await Vault.yieldAssets()).to.be.empty; // wbtc amount should be roughly equal to 0.075 ETH = 0.075 - const expectedWBTC = WBTCPerETH.mul(ethCollateral.mul(3).div(4)).div(ethers.utils.parseEther('1')); + const WBTCWithdrawal = WBTCPerETH.mul(ethCollateral.mul(3).div(4)).div(ethers.utils.parseEther('1')); + protocolFee = WBTCWithdrawal.mul(PROTOCOL_FEE_RATE).div(HUNDRED_PC); + const expectedWBTC = WBTCWithdrawal.sub(protocolFee); expect(getCollateralOf('WBTC', collateral).amount).to.equal(expectedWBTC); + expect(await WBTC.balanceOf(protocol.address)).to.equal(protocolFee); }); it('reverts when collateral asset is not compatible with given asset on withdrawal', async () => { @@ -677,5 +684,10 @@ describe('SmartVault', async () => { await expect(Vault.connect(user).withdrawYield(MockWETHWBTCHypervisor.address, ETH)).to.be.revertedWithCustomError(Vault, 'InvalidRequest'); }); + + it.only('only allows owner to set fee data', async() => { + await expect(YieldManager.connect(user).setFeeData(1000, ethers.constants.AddressZero)).to.be.revertedWithCustomError(YieldManager, 'InvalidUser'); + await expect(YieldManager.connect(admin).setFeeData(1000, ethers.constants.AddressZero)).not.to.be.reverted; + }); }); }); \ No newline at end of file From 82a2c55d6b95c818d7f893b638612dd91ed81eaf Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 21 Aug 2024 12:25:58 +0200 Subject: [PATCH 32/33] protect function which sets fee data --- contracts/SmartVaultYieldManager.sol | 2 +- test/SmartVault.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index f63e17b..a2acd3a 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -216,7 +216,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { delete hypervisorData[_collateralToken]; } - function setFeeData(uint256 _feeRate, address _smartVaultManager) external { + function setFeeData(uint256 _feeRate, address _smartVaultManager) external onlyOwner { feeRate = _feeRate; smartVaultManager = _smartVaultManager; } diff --git a/test/SmartVault.js b/test/SmartVault.js index 0a368cb..c277f01 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -685,8 +685,8 @@ describe('SmartVault', async () => { }); - it.only('only allows owner to set fee data', async() => { - await expect(YieldManager.connect(user).setFeeData(1000, ethers.constants.AddressZero)).to.be.revertedWithCustomError(YieldManager, 'InvalidUser'); + it('only allows owner to set fee data', async() => { + await expect(YieldManager.connect(user).setFeeData(1000, ethers.constants.AddressZero)).to.be.revertedWith('Ownable: caller is not the owner'); await expect(YieldManager.connect(admin).setFeeData(1000, ethers.constants.AddressZero)).not.to.be.reverted; }); }); From 3c147130ceb4b55f770cd71912f9786599d4ae8e Mon Sep 17 00:00:00 2001 From: Ewan Sheldon Date: Wed, 21 Aug 2024 12:31:24 +0200 Subject: [PATCH 33/33] protect function to add hypervisor --- contracts/SmartVaultYieldManager.sol | 2 +- test/SmartVault.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/contracts/SmartVaultYieldManager.sol b/contracts/SmartVaultYieldManager.sol index a2acd3a..2224abe 100644 --- a/contracts/SmartVaultYieldManager.sol +++ b/contracts/SmartVaultYieldManager.sol @@ -208,7 +208,7 @@ contract SmartVaultYieldManager is ISmartVaultYieldManager, Ownable { emit Withdraw(msg.sender, _token, _hypervisor, _withdrawn); } - function addHypervisorData(address _collateralToken, address _hypervisor, uint24 _poolFee, bytes memory _pathToEURA, bytes memory _pathFromEURA) external { + function addHypervisorData(address _collateralToken, address _hypervisor, uint24 _poolFee, bytes memory _pathToEURA, bytes memory _pathFromEURA) external onlyOwner { hypervisorData[_collateralToken] = HypervisorData(_hypervisor, _poolFee, _pathToEURA, _pathFromEURA); } diff --git a/test/SmartVault.js b/test/SmartVault.js index c277f01..4596ab1 100644 --- a/test/SmartVault.js +++ b/test/SmartVault.js @@ -651,11 +651,18 @@ describe('SmartVault', async () => { const MockUSDCWETHHypervisor = await (await ethers.getContractFactory('HypervisorMock')).deploy( 'USDC-WETH', 'USDC-WETH', USDC.address, MockWeth.address ); - await YieldManager.addHypervisorData( + // only allows own to set hypervisor data + await expect(YieldManager.connect(user).addHypervisorData( USDC.address, MockUSDCWETHHypervisor.address, 500, new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [USDC.address, 3000, EURA.address]), new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, USDC.address]) - ) + )).to.be.revertedWith('Ownable: caller is not the owner'); + + await expect(YieldManager.addHypervisorData( + USDC.address, MockUSDCWETHHypervisor.address, 500, + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [USDC.address, 3000, EURA.address]), + new ethers.utils.AbiCoder().encode(['address', 'uint24', 'address'], [EURA.address, 3000, USDC.address]) + )).not.to.be.reverted; // weth / wbtc hypervisor cannot be withdrawn to usdc, even tho there is usdc hypervisor data await expect(Vault.connect(user).withdrawYield(MockWETHWBTCHypervisor.address, ethers.utils.formatBytes32String('USDC')))