diff --git a/SlippageDerivation.md b/SlippageDerivation.md new file mode 100644 index 00000000..eeb5d8a8 --- /dev/null +++ b/SlippageDerivation.md @@ -0,0 +1,88 @@ +# Calculating Virtual CPMM LP by slippage and swap size + +Let a be the amount of x we are exchanging to get b amount of y. Therefore: + +(x + a) * (y - b) = k + +We want to solve for x. + +We know: + +et = execution price of the trade = b/a +ex = exchange rate before trade = y/x + +Let's now tune the liquidity to determine slippage. Let's say we want 1% slippage. + +We want et/ex = 0.99 + +So we have: + +et = 0.99*ex + +We know the exchange rate and we know b, so we can solve for a: + +b/a = 0.99*ex + +b/(0.99*ex) = a + +We know the exchange rate: + +ex = y/x + +now solve for y + +x*ex = y + +Here is our formula: + +(x + a) * (y - b) = x * y +xy - bx + ay - ab = x * y +-bx + ay - ab = 0 +ay = bx + ab + +solve for x: +ay - ab = bx +x = (ay - ab)/b +x = (ay)/b - a + +x = (a*x*ex)/b - a + +x = (b*x*ex)/(b*0.99*ex) - b/(0.99*ex) +x - (b*x*ex)/(b*0.99*ex) = - b/(0.99*ex) +x(1 - b*ex/(b*0.99*ex)) = - b/(0.99*ex) + +x = (-b / (0.99 * ex)) / (1 - b*ex/(b*0.99*ex)) +x = (b / (0.99 * ex)) / (b*ex/(b*0.99*ex) - 1) + + +if b = 100 +ex = 2 + +Then + +x = (-100 / (0.99*2)) / (1 - (100*2)/(100 *0.99*2)) +x = 5000 + +=> + +2 = y/5000 + +10000 = y + +Let's try it + +x = 5000 +y = 10000 +trading a of x for b of y. +b = 100 +solve for a + +ay = bx + ab +ay - ab = bx +a(y - b) = bx +a = bx / (y - b) +a = 100 * 5000 / (10000 - 100) +a = 50.5 + +Right on point! + diff --git a/contracts/PrizePoolLiquidator.sol b/contracts/PrizePoolLiquidator.sol index db6197e3..2752226c 100644 --- a/contracts/PrizePoolLiquidator.sol +++ b/contracts/PrizePoolLiquidator.sol @@ -121,7 +121,10 @@ contract PrizePoolLiquidator { _prizePool.award(msg.sender, amountOut); target.want.transferFrom(msg.sender, target.target, amountIn); - listener.afterSwap(_prizePool, _prizePool.getTicket(), amountOut, target.want, amountIn); + IPrizePoolLiquidatorListener _listener = listener; + if (address(_listener) != address(0)) { + _listener.afterSwap(_prizePool, _prizePool.getTicket(), amountOut, target.want, amountIn); + } return amountOut; } diff --git a/contracts/test/libraries/LiquidatorLibHarness.sol b/contracts/test/libraries/LiquidatorLibHarness.sol new file mode 100644 index 00000000..b35f9fed --- /dev/null +++ b/contracts/test/libraries/LiquidatorLibHarness.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +import "../../libraries/LiquidatorLib.sol"; + +contract LiquidatorLibHarness { + using LiquidatorLib for LiquidatorLib.State; + using SafeMath for uint256; + using SafeCast for uint256; + using PRBMathSD59x18Typed for PRBMath.SD59x18; + + LiquidatorLib.State state; + + function setState( + int256 exchangeRate, + uint256 lastSaleTime, + int256 deltaRatePerSecond, + int256 maxSlippage + ) external { + state = LiquidatorLib.State({ + exchangeRate: PRBMath.SD59x18(exchangeRate), + lastSaleTime: lastSaleTime, + deltaRatePerSecond: PRBMath.SD59x18(deltaRatePerSecond), + maxSlippage: PRBMath.SD59x18(maxSlippage) + }); + } + + function computeExchangeRate(uint256 _currentTime) external view returns (int256) { + return state.computeExchangeRate(_currentTime).value; + } + + function computeExactAmountInAtTime(uint256 availableBalance, uint256 amountOut, uint256 currentTime) external view returns (uint256) { + return state.computeExactAmountInAtTime(availableBalance, amountOut, currentTime); + } + + function computeExactAmountOutAtTime(uint256 availableBalance, uint256 amountIn, uint256 currentTime) external view returns (uint256) { + return state.computeExactAmountOutAtTime(availableBalance, amountIn, currentTime); + } + + function swapExactAmountInAtTime( + uint256 availableBalance, + uint256 amountIn, + uint256 currentTime + ) external returns (uint256) { + return state.swapExactAmountInAtTime(availableBalance, amountIn, currentTime); + } +} diff --git a/contracts/test/libraries/VirtualCpmmLibHarness.sol b/contracts/test/libraries/VirtualCpmmLibHarness.sol new file mode 100644 index 00000000..48748c99 --- /dev/null +++ b/contracts/test/libraries/VirtualCpmmLibHarness.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +import "@prb/math/contracts/PRBMathSD59x18.sol"; + +import "../../libraries/VirtualCpmmLib.sol"; + +contract VirtualCpmmLibHarness { + using SafeCast for uint256; + + function newCpmm( + int256 maxSlippage, + int256 exchangeRate, + uint256 haveAmount + ) external pure returns (VirtualCpmmLib.Cpmm memory) { + return VirtualCpmmLib.newCpmm( + PRBMath.SD59x18(maxSlippage), + PRBMath.SD59x18(exchangeRate), + PRBMathSD59x18Typed.fromInt(haveAmount.toInt256()) + ); + } + + // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset + function getAmountOut(uint amountIn, uint x, uint y) external pure returns (uint amountOut) { + return VirtualCpmmLib.getAmountOut(amountIn, x, y); + } + + // given an output amount of an asset and pair reserves, returns a required input amount of the other asset + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn) { + return VirtualCpmmLib.getAmountIn(amountOut, reserveIn, reserveOut); + } + +} diff --git a/test/libraries/LiquidatorLibHarness.test.ts b/test/libraries/LiquidatorLibHarness.test.ts new file mode 100644 index 00000000..c00757db --- /dev/null +++ b/test/libraries/LiquidatorLibHarness.test.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai'; +import { BigNumber, Contract, ContractFactory } from 'ethers'; +import { ethers } from 'hardhat'; + +const { utils } = ethers; +const { parseEther: toWei } = utils; + +describe('LiquidatorLibHarness', () => { + let liquidatorLibHarness: Contract; + let LiquidatorLibHarnessFactory: ContractFactory; + + before(async () => { + LiquidatorLibHarnessFactory = await ethers.getContractFactory('LiquidatorLibHarness'); + liquidatorLibHarness = await LiquidatorLibHarnessFactory.deploy(); + + const exchangeRate = toWei('2') // want:have + const lastSaleTime = '10' + const deltaRatePerSecond = toWei('0.01') // increases by 1% each second + const maxSlippage = toWei('0.01') + + await liquidatorLibHarness.setState( + exchangeRate, + lastSaleTime, + deltaRatePerSecond, + maxSlippage + ) + }) + + 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 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')) + }) + }) + + 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 return 0 if available balance is zero', async () => { + expect(await liquidatorLibHarness.computeExactAmountInAtTime('0', toWei('100'), '10')).to.equal('0') + }) + }) + + 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 return 0 if available balance is zero', async () => { + expect(await liquidatorLibHarness.computeExactAmountOutAtTime('0', toWei('100'), '10')).to.equal('0') + }) + }) + + describe('swapExactAmountInAtTime()', () => { + it('should swap correctly', async () => { + await liquidatorLibHarness.swapExactAmountInAtTime(toWei('1000'), toWei('100'), '10') + expect(await liquidatorLibHarness.computeExchangeRate('10')).to.equal('1992023936159616893') + }) + + 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') + }) + }) + +}); diff --git a/test/libraries/VirtualCpmmLibHarness.test.ts b/test/libraries/VirtualCpmmLibHarness.test.ts new file mode 100644 index 00000000..2737db50 --- /dev/null +++ b/test/libraries/VirtualCpmmLibHarness.test.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import { BigNumber, Contract, ContractFactory } from 'ethers'; +import { ethers } from 'hardhat'; + +const { utils } = ethers; +const { parseEther: toWei } = utils; + +describe('VirtualCpmmLibHarness', () => { + let virtualCpmmLibHarness: Contract; + let VirtualCpmmLibHarnessFactory: ContractFactory; + + before(async () => { + VirtualCpmmLibHarnessFactory = await ethers.getContractFactory('VirtualCpmmLibHarness'); + virtualCpmmLibHarness = await VirtualCpmmLibHarnessFactory.deploy(); + }); + + describe("newCpmm()", () => { + it("should have correct LP for one percent slippage", async () => { + + const cpmm = await virtualCpmmLibHarness.newCpmm( + toWei('0.01'), + toWei('2'), + '100' + ) + + expect(cpmm.want).to.equal('5000') + expect(cpmm.have).to.equal('10000') + + }) + + it('should have correct LP for ten percent slippage', async () => { + const cpmm = await virtualCpmmLibHarness.newCpmm( + toWei('0.1'), + toWei('2'), + '100' + ) + + expect(cpmm.want).to.equal('500') + expect(cpmm.have).to.equal('1000') + }) + }) + + describe("getAmountOut()", () => { + it('should be correct', async () => { + expect(await virtualCpmmLibHarness.getAmountOut(550, 5000, 10000)).to.equal(990) + }) + }) + + describe('getAmountIn()', () => { + it('should be correct', async () => { + expect(await virtualCpmmLibHarness.getAmountIn(990, 5000, 10000)).to.equal(549) + }) + }) +});