diff --git a/ArbTarget.md b/ArbTarget.md new file mode 100644 index 00000000..7807bb61 --- /dev/null +++ b/ArbTarget.md @@ -0,0 +1,22 @@ +Slippage: 5% +Initial Arb Target: 500 +Initial exchange rate: X + + +Then the arb target is when we expect them to buy to get a discount. It determines the LP size. + +After a user purchases, we set the new arb target based on the current arb + previous. + +Does this drive arb target to zero? + +PRBMath.SD59x18 memory one = PRBMath.SD59x18(1 ether); + +desiredExchangeRate = (1-slippage)*exchangeRate + +want = arbTarget.div(desiredExchangeRate).div( + arbTarget.mul(exchangeRate).div(arbTarget.mul(desiredExchangeRate)).sub(one) +); + +ay = bx + ab + +b = (ay)/(x+a) diff --git a/contracts/PrizePoolLiquidator.sol b/contracts/PrizePoolLiquidator.sol index 5ffbc4b5..88b17a4e 100644 --- a/contracts/PrizePoolLiquidator.sol +++ b/contracts/PrizePoolLiquidator.sol @@ -36,10 +36,10 @@ contract PrizePoolLiquidator { address target, IERC20 want, int256 exchangeRate, - int256 deltaRatePerSecond, - int256 maxSlippage + int256 maxSlippage, + uint256 haveArbTarget ) external returns (bool) { - return setPrizePoolAtTime(pool, target, want, exchangeRate, deltaRatePerSecond, maxSlippage, block.timestamp); + return setPrizePoolAtTime(pool, target, want, exchangeRate, maxSlippage, haveArbTarget); } function setPrizePoolAtTime( @@ -47,9 +47,8 @@ contract PrizePoolLiquidator { address target, IERC20 want, int256 exchangeRate, - int256 deltaRatePerSecond, int256 maxSlippage, - uint256 currentTime + uint256 haveArbTarget ) public returns (bool) { poolTargets[pool] = Target({ target: target, @@ -57,18 +56,13 @@ contract PrizePoolLiquidator { }); poolLiquidatorStates[pool] = LiquidatorLib.State({ exchangeRate: PRBMath.SD59x18(exchangeRate), - lastSaleTime: currentTime, - // positive price range change per second. - deltaRatePerSecond: PRBMath.SD59x18(deltaRatePerSecond), - // Price impact for purchase of accrued funds - // low slippage => higher frequency arbs, but it tracks the market rate slower (slower to change) - maxSlippage: PRBMath.SD59x18(maxSlippage) + maxSlippage: PRBMath.SD59x18(maxSlippage), + haveArbTarget: haveArbTarget }); return true; } - function setPrizePoolLiquidationState(IPrizePool _prizePool, int256 deltaRatePerSecond, int256 maxSlippage) external { - poolLiquidatorStates[_prizePool].deltaRatePerSecond = PRBMath.SD59x18(deltaRatePerSecond); + function setPrizePoolLiquidationState(IPrizePool _prizePool, int256 maxSlippage) external { poolLiquidatorStates[_prizePool].maxSlippage = PRBMath.SD59x18(maxSlippage); } @@ -84,34 +78,26 @@ contract PrizePoolLiquidator { time = _time; } - function currentExchangeRate(IPrizePool _prizePool) external view returns (int256) { - return poolLiquidatorStates[_prizePool].computeExchangeRate(block.timestamp).toInt(); + function currentExchangeRate(IPrizePool _prizePool) external returns (int256) { + return poolLiquidatorStates[_prizePool].computeExchangeRate(_availableStreamHaveBalance(_prizePool)).toInt(); } function computeExactAmountIn(IPrizePool _prizePool, uint256 amountOut) external returns (uint256) { - return poolLiquidatorStates[_prizePool].computeExactAmountInAtTime(_availableStreamHaveBalance(_prizePool), amountOut, block.timestamp); - } - - function computeExactAmountInAtTime(IPrizePool _prizePool, uint256 amountOut, uint256 currentTime) external returns (uint256) { - return poolLiquidatorStates[_prizePool].computeExactAmountInAtTime(_availableStreamHaveBalance(_prizePool), amountOut, currentTime); + return poolLiquidatorStates[_prizePool].computeExactAmountIn(_availableStreamHaveBalance(_prizePool), amountOut); } function computeExactAmountOut(IPrizePool _prizePool, uint256 amountIn) external returns (uint256) { - return poolLiquidatorStates[_prizePool].computeExactAmountOutAtTime(_availableStreamHaveBalance(_prizePool), amountIn, block.timestamp); - } - - function computeExactAmountOutAtTime(IPrizePool _prizePool, uint256 amountIn, uint256 currentTime) external returns (uint256) { - return poolLiquidatorStates[_prizePool].computeExactAmountOutAtTime(_availableStreamHaveBalance(_prizePool), amountIn, currentTime); + return poolLiquidatorStates[_prizePool].computeExactAmountOut(_availableStreamHaveBalance(_prizePool), amountIn); } function swapExactAmountIn(IPrizePool _prizePool, uint256 amountIn, uint256 amountOutMin) external returns (uint256) { - return _swapExactAmountInAtTime(_prizePool, amountIn, amountOutMin, block.timestamp); + return _swapExactAmountIn(_prizePool, amountIn, amountOutMin); } - function _swapExactAmountInAtTime(IPrizePool _prizePool, uint256 amountIn, uint256 amountOutMin, uint256 currentTime) internal returns (uint256) { + function _swapExactAmountIn(IPrizePool _prizePool, uint256 amountIn, uint256 amountOutMin) internal returns (uint256) { uint256 availableBalance = _availableStreamHaveBalance(_prizePool); - uint256 amountOut = poolLiquidatorStates[_prizePool].swapExactAmountInAtTime( - availableBalance, amountIn, currentTime + uint256 amountOut = poolLiquidatorStates[_prizePool].swapExactAmountIn( + availableBalance, amountIn ); require(amountOut <= availableBalance, "Whoops! have exceeds available"); @@ -123,18 +109,17 @@ contract PrizePoolLiquidator { } function swapExactAmountOut(IPrizePool _prizePool, uint256 amountOut, uint256 amountInMax) external returns (uint256) { - return _swapExactAmountOutAtTime(_prizePool, amountOut, amountInMax, block.timestamp); + return _swapExactAmountOut(_prizePool, amountOut, amountInMax); } - function _swapExactAmountOutAtTime( + function _swapExactAmountOut( IPrizePool _prizePool, uint256 amountOut, - uint256 amountInMax, - uint256 currentTime + uint256 amountInMax ) internal returns (uint256) { uint256 availableBalance = _availableStreamHaveBalance(_prizePool); - uint256 amountIn = poolLiquidatorStates[_prizePool].swapExactAmountOutAtTime( - availableBalance, amountOut, currentTime + uint256 amountIn = poolLiquidatorStates[_prizePool].swapExactAmountOut( + availableBalance, amountOut ); require(amountIn <= amountInMax, "trade does not meet min"); @@ -157,11 +142,9 @@ contract PrizePoolLiquidator { } function getLiquidationState(IPrizePool _prizePool) external view returns ( - int exchangeRate, - uint256 lastSaleTime + int exchangeRate ) { LiquidatorLib.State memory state = poolLiquidatorStates[_prizePool]; exchangeRate = state.exchangeRate.value; - lastSaleTime = state.lastSaleTime; } } diff --git a/contracts/libraries/LiquidatorLib.sol b/contracts/libraries/LiquidatorLib.sol index 71a44838..8b6f8d59 100644 --- a/contracts/libraries/LiquidatorLib.sol +++ b/contracts/libraries/LiquidatorLib.sol @@ -16,107 +16,77 @@ library LiquidatorLib { using PRBMathSD59x18Typed for PRBMath.SD59x18; struct State { - PRBMath.SD59x18 exchangeRate; - uint256 lastSaleTime; - // positive price range change per second. - PRBMath.SD59x18 deltaRatePerSecond; - // Price impact for purchase of accrued funds - // low slippage => higher frequency arbs, but it tracks the market rate slower (slower to change) PRBMath.SD59x18 maxSlippage; + PRBMath.SD59x18 exchangeRate; + uint256 haveArbTarget; } - function _increaseExchangeRateByDeltaTime( - PRBMath.SD59x18 memory exchangeRate, - PRBMath.SD59x18 memory deltaRatePerSecond, - uint256 lastSaleTime, - uint256 currentTime - ) internal pure returns (PRBMath.SD59x18 memory) { - // over time, buying power of POOL goes up. - PRBMath.SD59x18 memory dt = PRBMathSD59x18Typed.fromInt( - (currentTime - lastSaleTime).toInt256() - ); - return exchangeRate.add(exchangeRate.mul(dt.mul(deltaRatePerSecond))); - } - - function computeExchangeRate(State storage _liquidationState, uint256 _currentTime) + function computeExchangeRate(State storage _liquidationState, uint256 availableBalance) internal view returns (PRBMath.SD59x18 memory) { - return - _increaseExchangeRateByDeltaTime( - _liquidationState.exchangeRate, - _liquidationState.deltaRatePerSecond, - _liquidationState.lastSaleTime, - _currentTime - ); + VirtualCpmmLib.Cpmm memory cpmm = _computeCpmm(_liquidationState, availableBalance); + return _cpmmToExchangeRate(cpmm); } - function computeExactAmountInAtTime( + function computeExactAmountIn( State storage _liquidationState, uint256 availableBalance, - uint256 amountOut, - uint256 currentTime + uint256 amountOut ) internal pure returns (uint256) { - if (availableBalance == 0) { - return 0; - } + require(amountOut <= availableBalance, "insuff balance"); VirtualCpmmLib.Cpmm memory cpmm = _computeCpmm( _liquidationState, - availableBalance, - currentTime + availableBalance ); return VirtualCpmmLib.getAmountIn(amountOut, cpmm.want, cpmm.have); } - function computeExactAmountOutAtTime( + function computeExactAmountOut( State storage _liquidationState, uint256 availableBalance, - uint256 amountIn, - uint256 currentTime + uint256 amountIn ) internal pure returns (uint256) { - if (availableBalance == 0) { - return 0; - } VirtualCpmmLib.Cpmm memory cpmm = _computeCpmm( _liquidationState, - availableBalance, - currentTime + availableBalance ); - return VirtualCpmmLib.getAmountOut(amountIn, cpmm.want, cpmm.have); + uint256 amountOut = VirtualCpmmLib.getAmountOut(amountIn, cpmm.want, cpmm.have); + require(amountOut <= availableBalance, "insuff balance"); + return amountOut; } function _computeCpmm( State storage _liquidationState, - uint256 availableBalance, - uint256 currentTime + uint256 availableBalance ) internal pure returns (VirtualCpmmLib.Cpmm memory) { State memory liquidationState = _liquidationState; - PRBMath.SD59x18 memory newExchangeRate = _increaseExchangeRateByDeltaTime( + VirtualCpmmLib.Cpmm memory cpmm = VirtualCpmmLib.newCpmm( + liquidationState.maxSlippage, liquidationState.exchangeRate, - liquidationState.deltaRatePerSecond, - liquidationState.lastSaleTime, - currentTime + PRBMathSD59x18Typed.fromInt(liquidationState.haveArbTarget.toInt256()) ); - return - VirtualCpmmLib.newCpmm( - liquidationState.maxSlippage, - newExchangeRate, - PRBMathSD59x18Typed.fromInt(availableBalance.toInt256()) - ); + + // Now we swap available balance for POOL + + uint256 wantAmount = VirtualCpmmLib.getAmountOut(availableBalance, cpmm.have, cpmm.want); + + cpmm.want -= wantAmount; + cpmm.have += availableBalance; + + return cpmm; } - function swapExactAmountInAtTime( + function swapExactAmountIn( State storage liquidationState, uint256 availableBalance, - uint256 amountIn, - uint256 currentTime + uint256 amountIn ) internal returns (uint256) { require(availableBalance > 0, "Whoops! no funds available"); VirtualCpmmLib.Cpmm memory cpmm = _computeCpmm( liquidationState, - availableBalance, - currentTime + availableBalance ); uint256 amountOut = VirtualCpmmLib.getAmountOut(amountIn, cpmm.want, cpmm.have); @@ -125,23 +95,20 @@ library LiquidatorLib { require(amountOut <= availableBalance, "Whoops! have exceeds available"); - liquidationState.lastSaleTime = currentTime; liquidationState.exchangeRate = _cpmmToExchangeRate(cpmm); return amountOut; } - function swapExactAmountOutAtTime( + function swapExactAmountOut( State storage liquidationState, uint256 availableBalance, - uint256 amountOut, - uint256 currentTime + uint256 amountOut ) internal returns (uint256) { require(availableBalance > 0, "Whoops! no funds available"); VirtualCpmmLib.Cpmm memory cpmm = _computeCpmm( liquidationState, - availableBalance, - currentTime + availableBalance ); uint256 amountIn = VirtualCpmmLib.getAmountIn(amountOut, cpmm.want, cpmm.have); @@ -150,7 +117,6 @@ library LiquidatorLib { require(amountOut <= availableBalance, "Whoops! have exceeds available"); - liquidationState.lastSaleTime = currentTime; liquidationState.exchangeRate = _cpmmToExchangeRate(cpmm); return amountIn; diff --git a/contracts/test/PrizePoolLiquidatorHarness.sol b/contracts/test/PrizePoolLiquidatorHarness.sol index 264f4f15..02acea0f 100644 --- a/contracts/test/PrizePoolLiquidatorHarness.sol +++ b/contracts/test/PrizePoolLiquidatorHarness.sol @@ -5,17 +5,4 @@ pragma solidity 0.8.6; import "../PrizePoolLiquidator.sol"; contract PrizePoolLiquidatorHarness is PrizePoolLiquidator { - - function swapExactAmountInAtTime(IPrizePool _prizePool, uint256 amountIn, uint256 amountOutMin, uint256 currentTime) external returns (uint256) { - return _swapExactAmountInAtTime(_prizePool, amountIn, amountOutMin, currentTime); - } - - function swapExactAmountOutAtTime( - IPrizePool _prizePool, - uint256 amountOut, - uint256 amountInMax, - uint256 currentTime - ) external returns (uint256) { - return _swapExactAmountOutAtTime(_prizePool, amountOut, amountInMax, currentTime); - } } diff --git a/contracts/test/libraries/LiquidatorLibHarness.sol b/contracts/test/libraries/LiquidatorLibHarness.sol index ebbbb88e..0cdd1afe 100644 --- a/contracts/test/libraries/LiquidatorLibHarness.sol +++ b/contracts/test/libraries/LiquidatorLibHarness.sol @@ -18,46 +18,42 @@ contract LiquidatorLibHarness { function setState( int256 exchangeRate, - uint256 lastSaleTime, - int256 deltaRatePerSecond, - int256 maxSlippage + int256 maxSlippage, + uint256 haveArbTarget ) external { state = LiquidatorLib.State({ exchangeRate: PRBMath.SD59x18(exchangeRate), - lastSaleTime: lastSaleTime, - deltaRatePerSecond: PRBMath.SD59x18(deltaRatePerSecond), - maxSlippage: PRBMath.SD59x18(maxSlippage) + maxSlippage: PRBMath.SD59x18(maxSlippage), + haveArbTarget: haveArbTarget }); } - function computeExchangeRate(uint256 _currentTime) external view returns (int256) { - return state.computeExchangeRate(_currentTime).value; + function computeExchangeRate(uint256 availableBalance) external view returns (int256) { + return state.computeExchangeRate(availableBalance).value; } - function computeExactAmountInAtTime(uint256 availableBalance, uint256 amountOut, uint256 currentTime) external view returns (uint256) { - return state.computeExactAmountInAtTime(availableBalance, amountOut, currentTime); + function computeExactAmountIn(uint256 availableBalance, uint256 amountOut) external view returns (uint256) { + return state.computeExactAmountIn(availableBalance, amountOut); } - function computeExactAmountOutAtTime(uint256 availableBalance, uint256 amountIn, uint256 currentTime) external view returns (uint256) { - return state.computeExactAmountOutAtTime(availableBalance, amountIn, currentTime); + function computeExactAmountOut(uint256 availableBalance, uint256 amountIn) external view returns (uint256) { + return state.computeExactAmountOut(availableBalance, amountIn); } - function swapExactAmountInAtTime( + function swapExactAmountIn( uint256 availableBalance, - uint256 amountIn, - uint256 currentTime + uint256 amountIn ) external returns (uint256) { - uint256 result = state.swapExactAmountInAtTime(availableBalance, amountIn, currentTime); + uint256 result = state.swapExactAmountIn(availableBalance, amountIn); emit SwapResult(result); return result; } - function swapExactAmountOutAtTime( + function swapExactAmountOut( uint256 availableBalance, - uint256 amountOut, - uint256 currentTime + uint256 amountOut ) external returns (uint256) { - uint256 result = state.swapExactAmountOutAtTime(availableBalance, amountOut, currentTime); + uint256 result = state.swapExactAmountOut(availableBalance, amountOut); emit SwapResult(result); return result; } diff --git a/test/libraries/LiquidatorLibHarness.test.ts b/test/libraries/LiquidatorLibHarness.test.ts index 5552bc48..9b0b0041 100644 --- a/test/libraries/LiquidatorLibHarness.test.ts +++ b/test/libraries/LiquidatorLibHarness.test.ts @@ -16,77 +16,70 @@ describe('LiquidatorLibHarness', () => { beforeEach(async () => { const exchangeRate = toWei('2') // want:have - const lastSaleTime = '10' - const deltaRatePerSecond = toWei('0.01') // increases by 1% each second - const maxSlippage = toWei('0.01') + const maxSlippage = toWei('0.01') // 1 percent slippage + const arbTarget = toWei('100') await liquidatorLibHarness.setState( exchangeRate, - lastSaleTime, - deltaRatePerSecond, - maxSlippage + maxSlippage, + arbTarget ) }) describe('computeExchangeRate()', () => { - it('should start at the current exchange rate when delta time is zero', async () => { - expect(await liquidatorLibHarness.computeExchangeRate('10')).to.equal(toWei('2')) + it('should have the current exchange rate when no available balance', async () => { + expect(await liquidatorLibHarness.computeExchangeRate('0')).to.equal(toWei('2')) }) - it('should increase the exchange rate by delta time', async () => { - // 10 seconds, 1 percent each second, => Delta exchange rate = 10% x 2 = 0.2 - // = 2 + 0.2 = 2.2 - expect(await liquidatorLibHarness.computeExchangeRate('20')).to.equal(toWei('2.2')) + it('should have the expected slippage when arb target is matched', async () => { + // exchange rate is have / want + // higher means more USDC per POOL + expect(await liquidatorLibHarness.computeExchangeRate(toWei('100'))).to.equal('2040199999999999999') }) - }) - describe('computeExactAmountInAtTime()', () => { - it('should compute how much can be purchased at time = 0', async () => { - expect(await liquidatorLibHarness.computeExactAmountInAtTime(toWei('1000'), toWei('100'), '10')).to.equal('50050050050050050049') + it('should handle insane available balance amounts', async () => { + expect(await liquidatorLibHarness.computeExchangeRate(toWei('10000000'))).to.equal('2004001999999999995987985') }) + }) - it('should return 0 if available balance is zero', async () => { - expect(await liquidatorLibHarness.computeExactAmountInAtTime('0', toWei('100'), '10')).to.equal('0') + describe('computeExactAmountIn()', () => { + it('should revert when they request more than what is available', async () => { + await expect(liquidatorLibHarness.computeExactAmountIn(toWei('100'), toWei('110'))).to.be.revertedWith('insuff balance') }) - }) - describe('computeExactAmountOutAtTime()', () => { - it('should compute how much can be purchased at time = 0', async () => { - expect(await liquidatorLibHarness.computeExactAmountOutAtTime(toWei('1000'), toWei('50'), '10')).to.equal('99900099900099900099') + it('should work when availableBalance exceeds arb target', async () => { + expect(await liquidatorLibHarness.computeExactAmountIn(toWei('1000'), toWei('100'))).to.equal('41701417848206839039') }) + }) - it('should return 0 if available balance is zero', async () => { - expect(await liquidatorLibHarness.computeExactAmountOutAtTime('0', toWei('100'), '10')).to.equal('0') + describe('computeExactAmountOut()', () => { + it('should revert when requesting more than avail', async () => { + await expect(liquidatorLibHarness.computeExactAmountOut(toWei('100'), toWei('500'))).to.be.revertedWith('insuff balance') }) }) - describe('swapExactAmountInAtTime()', () => { + describe('swapExactAmountIn()', () => { it('should swap correctly', async () => { - await expect(liquidatorLibHarness.swapExactAmountInAtTime(toWei('1000'), toWei('50'), '10')) - .to.emit(liquidatorLibHarness, 'SwapResult').withArgs('99900099900099900099') - expect(await liquidatorLibHarness.computeExchangeRate('10')).to.equal('1996005992009988013') + await expect(liquidatorLibHarness.swapExactAmountIn(toWei('100'), toWei('45'))) + .to.emit(liquidatorLibHarness, 'SwapResult').withArgs('90981973857634105975') + expect(await liquidatorLibHarness.computeExchangeRate('10')).to.equal('2003608836952856445') }) it('should revert if there is insufficient balance', async () => { - await expect(liquidatorLibHarness.swapExactAmountInAtTime(toWei('50'), toWei('100'), '10')).to.be.revertedWith('Whoops! have exceeds available') + await expect(liquidatorLibHarness.swapExactAmountIn(toWei('50'), toWei('100'))).to.be.revertedWith('Whoops! have exceeds available') }) }) - describe('swapExactAmountOutAtTime()', () => { + describe('swapExactAmountOut()', () => { it('should update the exchange rate', async () => { - await expect(liquidatorLibHarness.swapExactAmountOutAtTime(toWei('1000'), toWei('1000'), '10')) - .to.emit(liquidatorLibHarness, 'SwapResult').withArgs('505050505050505050499') - expect(await liquidatorLibHarness.computeExchangeRate('10')).to.equal(toWei('1.9602')) - }) - - it('should update the last sale timestamp', async () => { - await liquidatorLibHarness.swapExactAmountOutAtTime(toWei('1000'), toWei('1000'), '20') - const state = await liquidatorLibHarness.state() - expect(state.lastSaleTime.toString()).to.equal('20') + await expect(liquidatorLibHarness.swapExactAmountOut(toWei('100'), toWei('100'))) + .to.emit(liquidatorLibHarness, 'SwapResult').withArgs('49504950495049504950') + // now that everything has been liquidated, the exchange rate should be driven back down + expect(await liquidatorLibHarness.computeExchangeRate('0')).to.equal(toWei('2')) }) it('should revert if there is insufficient balance', async () => { - await expect(liquidatorLibHarness.swapExactAmountOutAtTime(toWei('50'), toWei('100'), '10')) + await expect(liquidatorLibHarness.swapExactAmountOut(toWei('50'), toWei('100'))) .to.be.revertedWith('Whoops! have exceeds available') }) })