diff --git a/contracts/interfaces/ICurvePoolFactory.sol b/contracts/interfaces/ICurvePoolFactory.sol new file mode 100644 index 000000000..aad4f9481 --- /dev/null +++ b/contracts/interfaces/ICurvePoolFactory.sol @@ -0,0 +1,15 @@ +interface ICurvePoolFactory { + function get_coins(address _pool) external view returns (address[4] memory); + + function deploy_plain_pool( + string memory _name, + string memory _symbol, + address[4] memory _coins, + uint256 _A, + uint256 _fee + ) external returns (address); + + function pool_list(uint256 _arg) external view returns (address); + + function pool_count() external view returns (uint256); +} diff --git a/contracts/peripheral/IncurDebt.sol b/contracts/peripheral/IncurDebt.sol index e9e77922b..009589c87 100644 --- a/contracts/peripheral/IncurDebt.sol +++ b/contracts/peripheral/IncurDebt.sol @@ -353,6 +353,7 @@ contract IncurDebt is OlympusAccessControlledV2, IIncurDebt { revert IncurDebt_AmountAboveBorrowerBalance(_liquidity); lpTokenOwnership[_lpToken][msg.sender] -= _liquidity; + IERC20(_lpToken).safeTransfer(_strategy, _liquidity); ohmRecieved = IStrategy(_strategy).removeLiquidity(_strategyParams, _liquidity, _lpToken, msg.sender); @@ -384,6 +385,7 @@ contract IncurDebt is OlympusAccessControlledV2, IIncurDebt { // borrower can decide to call repayDebtWithOHM() and clear debt if (borrowers[msg.sender].debt != 0) repayDebtWithCollateral(); + lpTokenOwnership[_lpToken][msg.sender] -= _liquidity; IERC20(_lpToken).safeTransfer(msg.sender, _liquidity); diff --git a/contracts/peripheral/Strategies/Curve.sol b/contracts/peripheral/Strategies/Curve.sol index f6166e62b..5ee618aea 100644 --- a/contracts/peripheral/Strategies/Curve.sol +++ b/contracts/peripheral/Strategies/Curve.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.10; import "../../interfaces/IERC20.sol"; import "../../libraries/SafeERC20.sol"; import "../../interfaces/IStrategy.sol"; +import "../../interfaces/ICurvePoolFactory.sol"; interface ICurvePool { function add_liquidity(uint256[2] memory _deposit_amounts, uint256 _min_mint_amount) external returns (uint256); @@ -13,10 +14,6 @@ interface ICurvePool { returns (uint256[2] memory); } -interface ICurveFactory { - function get_coins(address _pool) external view returns (address[8] memory); -} - error CurveStrategy_NotIncurDebtAddress(); error CurveStrategy_AmountsDoNotMatch(); error CurveStrategy_LPTokenDoesNotMatch(); @@ -29,7 +26,7 @@ error CurveStrategy_OhmAddressNotFound(); contract CurveStrategy is IStrategy { using SafeERC20 for IERC20; - ICurveFactory factory; + ICurvePoolFactory factory; address public immutable incurDebtAddress; address public immutable ohmAddress; @@ -38,7 +35,7 @@ contract CurveStrategy is IStrategy { address _ohmAddress, address _factory ) { - factory = ICurveFactory(_factory); + factory = ICurvePoolFactory(_factory); incurDebtAddress = _incurDebtAddress; ohmAddress = _ohmAddress; } @@ -63,7 +60,7 @@ contract CurveStrategy is IStrategy { (uint256[2] memory amounts, uint256 min_mint_amount, address pairTokenAddress, address poolAddress) = abi .decode(_data, (uint256[2], uint256, address, address)); - address[8] memory poolTokens = factory.get_coins(poolAddress); + address[4] memory poolTokens = factory.get_coins(poolAddress); if (poolTokens[0] == ohmAddress) { if (poolTokens[1] != pairTokenAddress) revert CurveStrategy_LPTokenDoesNotMatch(); @@ -85,9 +82,11 @@ contract CurveStrategy is IStrategy { revert CurveStrategy_LPTokenDoesNotMatch(); } + IERC20(ohmAddress).approve(poolAddress, _ohmAmount); liquidity = ICurvePool(poolAddress).add_liquidity(amounts, min_mint_amount); // Ohm unused will be 0 since curve uses up all input tokens for LP. lpTokenAddress = poolAddress; // For factory pools on curve, the LP token is the pool contract. + IERC20(lpTokenAddress).safeTransfer(incurDebtAddress, liquidity); } function removeLiquidity( @@ -102,11 +101,9 @@ contract CurveStrategy is IStrategy { if (_burn_amount != _liquidity) revert CurveStrategy_AmountsDoNotMatch(); - // probably dont need to but test if need approve token to pool before remove lp. if all good remove this comment. - uint256[2] memory resultAmounts = ICurvePool(_lpTokenAddress).remove_liquidity(_burn_amount, _min_amounts); - address[8] memory poolTokens = factory.get_coins(_lpTokenAddress); + address[4] memory poolTokens = factory.get_coins(_lpTokenAddress); if (poolTokens[0] == ohmAddress) { ohmRecieved = resultAmounts[0]; diff --git a/test/debt/IncurDebt.js b/test/debt/IncurDebt.js index 1730b92c0..295a0fcc3 100644 --- a/test/debt/IncurDebt.js +++ b/test/debt/IncurDebt.js @@ -34,7 +34,12 @@ describe("IncurDebt", async () => { UniSwapStrategy, uniSwapStrategy, uniswapLpContract, - uniOhmDaiLpAddress; + uniOhmDaiLpAddress, + curveStrategyFactory, + curveStrategy, + curvePoolFactory; + + let zeroAddress = "0x0000000000000000000000000000000000000000"; beforeEach(async () => { await fork_network(14565910); @@ -47,6 +52,7 @@ describe("IncurDebt", async () => { uniRouter = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; factory = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"; uniOhmDaiLpAddress = "0x1b851374b8968393c11e8fb30c2842cfc4e986a5"; + curveFactoryAddress = "0xB9fC157394Af804a3578134A6585C0dc9cc990d4"; IncurDebt = await ethers.getContractFactory("IncurDebt"); incurDebt = await IncurDebt.deploy( @@ -66,6 +72,15 @@ describe("IncurDebt", async () => { olympus.ohm ); + curveStrategyFactory = await ethers.getContractFactory("CurveStrategy"); + curveStrategy = await curveStrategyFactory.deploy( + incurDebt.address, + olympus.ohm, + curveFactoryAddress + ); + + curvePoolFactory = await ethers.getContractAt("ICurvePoolFactory", curveFactoryAddress); + daiContract = await ethers.getContractAt( "contracts/interfaces/IERC20.sol:IERC20", "0x6B175474E89094C44Da98b954EedeAC495271d0F" @@ -778,6 +793,94 @@ describe("IncurDebt", async () => { incurDebt.connect(daiHolder).createLP(ohmAmount, uniSwapStrategy.address, data) ).to.emit(incurDebt, "LpInteraction"); }); + + it("Should allow borrower create lp for curve", async () => { + await curvePoolFactory.deploy_plain_pool( + "Test Ohm Dai Pool", + "TEST", + [olympus.ohm, olympus.sohm, zeroAddress, zeroAddress], + 10, + 4000000 + ); + let poolCount = await curvePoolFactory.pool_count(); + let lpPoolAddress = await curvePoolFactory.pool_list(poolCount.sub(1)); + + const curveParaData = ethers.utils.defaultAbiCoder.encode( + ["uint256[2]", "uint256", "address", "address"], + [[ohmAmount, ohmAmount], 0, olympus.sohm, lpPoolAddress] + ); + + await incurDebt.connect(governor).setGlobalDebtLimit(amount); + await incurDebt.connect(governor).allowBorrower(gOhmHolder.address, true, false); + + await incurDebt.connect(governor).setBorrowerDebtLimit(gOhmHolder.address, ohmAmount); + + await gohm_token.connect(gOhmHolder).approve(incurDebt.address, amountInGOHM); + await sohm_token.connect(sOhmHolder).transfer(gOhmHolder.address, ohmAmount); + + await incurDebt.connect(gOhmHolder).deposit(amountInGOHM); + await treasury.connect(governor).setDebtLimit(incurDebt.address, amount); + + await incurDebt.connect(governor).whitelistStrategy(curveStrategy.address); + await sohm_token.connect(gOhmHolder).approve(curveStrategy.address, ohmAmount); + await incurDebt + .connect(gOhmHolder) + .createLP(ohmAmount, curveStrategy.address, curveParaData); + + let liquidityAmount = await incurDebt.lpTokenOwnership( + lpPoolAddress, + gOhmHolder.address + ); + + await expect(liquidityAmount).to.equal("66000000000000000000"); + await expect(await incurDebt.totalOutstandingGlobalDebt()).to.equal(ohmAmount); + }); + + it("curve strategy should revert when wrong lp address or amount", async () => { + await curvePoolFactory.deploy_plain_pool( + "Test Ohm Dai Pool", + "TEST", + [olympus.ohm, olympus.sohm, zeroAddress, zeroAddress], + 10, + 4000000 + ); + let poolCount = await curvePoolFactory.pool_count(); + let lpPoolAddress = await curvePoolFactory.pool_list(poolCount.sub(1)); + + let curveParaData = ethers.utils.defaultAbiCoder.encode( + ["uint256[2]", "uint256", "address", "address"], + [[ohmAmount, ohmAmount], 0, olympus.sohm, olympus.ohm] + ); + + await incurDebt.connect(governor).setGlobalDebtLimit(amount); + await incurDebt.connect(governor).allowBorrower(gOhmHolder.address, true, false); + + await incurDebt.connect(governor).setBorrowerDebtLimit(gOhmHolder.address, ohmAmount); + + await gohm_token.connect(gOhmHolder).approve(incurDebt.address, amountInGOHM); + await sohm_token.connect(sOhmHolder).transfer(gOhmHolder.address, ohmAmount); + + await incurDebt.connect(gOhmHolder).deposit(amountInGOHM); + await treasury.connect(governor).setDebtLimit(incurDebt.address, amount); + + await incurDebt.connect(governor).whitelistStrategy(curveStrategy.address); + await sohm_token.connect(gOhmHolder).approve(curveStrategy.address, ohmAmount); + expect( + incurDebt + .connect(gOhmHolder) + .createLP(ohmAmount, curveStrategy.address, curveParaData) + ).to.revertedWith("CurveStrategy_LPTokenDoesNotMatch()"); + + curveParaData = ethers.utils.defaultAbiCoder.encode( + ["uint256[2]", "uint256", "address", "address"], + [[ohmAmount, "1230124902"], 0, olympus.sohm, lpPoolAddress] + ); + expect( + incurDebt + .connect(gOhmHolder) + .createLP(ohmAmount, curveStrategy.address, curveParaData) + ).to.revertedWith("CurveStrategy_AmountsDoNotMatch()"); + }); }); describe("function removeLP(_liquidity, _strategy, _lpToken, _strategyParams)", () => { @@ -875,6 +978,61 @@ describe("IncurDebt", async () => { const totalOutstandingGlobalDebtAfterTx = await incurDebt.totalOutstandingGlobalDebt(); assert.equal(Number(totalOutstandingGlobalDebtAfterTx), 1); }); + + it("Should allow borrower remove lp for curve", async () => { + await curvePoolFactory.deploy_plain_pool( + "Test Ohm Dai Pool", + "TEST", + [olympus.ohm, olympus.sohm, zeroAddress, zeroAddress], + 10, + 4000000 + ); + let poolCount = await curvePoolFactory.pool_count(); + let lpPoolAddress = await curvePoolFactory.pool_list(poolCount.sub(1)); + + const curveParaData = ethers.utils.defaultAbiCoder.encode( + ["uint256[2]", "uint256", "address", "address"], + [[ohmAmount, ohmAmount], 0, olympus.sohm, lpPoolAddress] + ); + + await incurDebt.connect(governor).setGlobalDebtLimit(amount); + await incurDebt.connect(governor).allowBorrower(gOhmHolder.address, true, false); + + await incurDebt.connect(governor).setBorrowerDebtLimit(gOhmHolder.address, ohmAmount); + + await gohm_token.connect(gOhmHolder).approve(incurDebt.address, amountInGOHM); + await sohm_token.connect(sOhmHolder).transfer(gOhmHolder.address, ohmAmount); + + await incurDebt.connect(gOhmHolder).deposit(amountInGOHM); + await treasury.connect(governor).setDebtLimit(incurDebt.address, amount); + + await incurDebt.connect(governor).whitelistStrategy(curveStrategy.address); + await sohm_token.connect(gOhmHolder).approve(curveStrategy.address, ohmAmount); + await incurDebt + .connect(gOhmHolder) + .createLP(ohmAmount, curveStrategy.address, curveParaData); + + let liquidityAmount = await incurDebt.lpTokenOwnership( + lpPoolAddress, + gOhmHolder.address + ); + + let sOhmAmountBeforeRemove = await sohm_token.balanceOf(gOhmHolder.address); + + const curveRemoveData = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint256[2]"], + [liquidityAmount, [0, 0]] + ); + + await incurDebt + .connect(gOhmHolder) + .removeLP(liquidityAmount, curveStrategy.address, lpPoolAddress, curveRemoveData); + + let sOhmAmountAfterRemove = await sohm_token.balanceOf(gOhmHolder.address); + + await expect(sOhmAmountAfterRemove.sub(sOhmAmountBeforeRemove)).to.equal(ohmAmount); + await expect(await incurDebt.totalOutstandingGlobalDebt()).to.equal(0); + }); }); describe("withdrawLP(uint256 _liquidity, address _lpToken)", async () => { @@ -978,6 +1136,60 @@ describe("IncurDebt", async () => { const borrowerLpBalanceAfterTx = await uniswapLpContract.balanceOf(daiHolder.address); assert.equal(Number(borrowerLpBalanceAfterTx), Number(borrowerLpBeforeTx)); }); + + it("Should allow borrower withdraw lp for curve", async () => { + await curvePoolFactory.deploy_plain_pool( + "Test Ohm Dai Pool", + "TEST", + [olympus.ohm, olympus.sohm, zeroAddress, zeroAddress], + 10, + 4000000 + ); + let poolCount = await curvePoolFactory.pool_count(); + let lpPoolAddress = await curvePoolFactory.pool_list(poolCount.sub(1)); + + const curveParaData = ethers.utils.defaultAbiCoder.encode( + ["uint256[2]", "uint256", "address", "address"], + [[ohmAmount, ohmAmount], 0, olympus.sohm, lpPoolAddress] + ); + + await incurDebt.connect(governor).setGlobalDebtLimit(amount); + await incurDebt.connect(governor).allowBorrower(gOhmHolder.address, true, false); + + await incurDebt.connect(governor).setBorrowerDebtLimit(gOhmHolder.address, ohmAmount); + + await gohm_token.connect(gOhmHolder).approve(incurDebt.address, amountInGOHM); + await sohm_token.connect(sOhmHolder).transfer(gOhmHolder.address, ohmAmount); + + await incurDebt.connect(gOhmHolder).deposit(amountInGOHM); + await treasury.connect(governor).setDebtLimit(incurDebt.address, amount); + + await incurDebt.connect(governor).whitelistStrategy(curveStrategy.address); + await sohm_token.connect(gOhmHolder).approve(curveStrategy.address, ohmAmount); + await incurDebt + .connect(gOhmHolder) + .createLP(ohmAmount, curveStrategy.address, curveParaData); + + let liquidityAmount = await incurDebt.lpTokenOwnership( + lpPoolAddress, + gOhmHolder.address + ); + + await incurDebt.connect(gOhmHolder).repayDebtWithCollateral(); + + let lpTokenContract = await ethers.getContractAt( + "contracts/interfaces/IERC20.sol:IERC20", + lpPoolAddress + ); + + await expect(await lpTokenContract.balanceOf(gOhmHolder.address)).to.equal(0); + + await incurDebt.connect(gOhmHolder).withdrawLP(liquidityAmount, lpPoolAddress); + + await expect(await lpTokenContract.balanceOf(gOhmHolder.address)).to.equal( + liquidityAmount + ); + }); }); async function impersonate(address) {