From 215ee8cc2b672fb0f073b1985be91189932c9e33 Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Fri, 24 Sep 2021 09:48:53 -0300 Subject: [PATCH] Phantom: Test swaps (#864) * phantom: test swaps * phantom: fix linter * remove hardhat console import Co-authored-by: dmf7z --- .../contracts/StablePhantomPool.sol | 86 +++---- .../test/StablePhantomPool.test.ts | 238 +++++++++++++++--- .../pools/stable-phantom/StablePhantomPool.ts | 139 ++++++++-- .../StablePhantomPoolDeployer.ts | 18 +- .../src/models/pools/stable-phantom/types.ts | 7 +- pvt/helpers/src/models/pools/stable/math.ts | 8 +- pvt/helpers/src/models/vault/Vault.ts | 61 +++-- 7 files changed, 434 insertions(+), 123 deletions(-) diff --git a/pkg/pool-stable-phantom/contracts/StablePhantomPool.sol b/pkg/pool-stable-phantom/contracts/StablePhantomPool.sol index 4bc03bd1ca..935221f09f 100644 --- a/pkg/pool-stable-phantom/contracts/StablePhantomPool.sol +++ b/pkg/pool-stable-phantom/contracts/StablePhantomPool.sol @@ -126,6 +126,7 @@ contract StablePhantomPool is StablePool { uint256 indexIn, uint256 indexOut ) public virtual override onlyVault(request.poolId) returns (uint256) { + _require(totalSupply() > 0, Errors.UNINITIALIZED); _cachePriceRatesIfNecessary(); return super.onSwap(request, balances, indexIn, indexOut); } @@ -139,11 +140,13 @@ contract StablePhantomPool is StablePool { uint256 indexIn, uint256 indexOut ) internal virtual override returns (uint256) { - uint256[] memory balances = _dropBptItem(balancesIncludingBpt); // Avoid BPT balance for stable pool math + // Avoid BPT balance for stable pool math + (uint256 virtualSupply, uint256[] memory balances) = _dropBptItem(balancesIncludingBpt); + if (request.tokenIn == IERC20(this)) { - return _onSwapTokenGivenBptIn(request.amount, _skipBptIndex(indexOut), balances); + return _onSwapTokenGivenBptIn(request.amount, _skipBptIndex(indexOut), virtualSupply, balances); } else if (request.tokenOut == IERC20(this)) { - return _onSwapBptGivenTokenIn(request.amount, _skipBptIndex(indexIn), balances); + return _onSwapBptGivenTokenIn(request.amount, _skipBptIndex(indexIn), virtualSupply, balances); } else { return super._onSwapGivenIn(request, balances, _skipBptIndex(indexIn), _skipBptIndex(indexOut)); } @@ -158,11 +161,13 @@ contract StablePhantomPool is StablePool { uint256 indexIn, uint256 indexOut ) internal virtual override returns (uint256) { - uint256[] memory balances = _dropBptItem(balancesIncludingBpt); // Avoid BPT balance for stable pool math + // Avoid BPT balance for stable pool math + (uint256 virtualSupply, uint256[] memory balances) = _dropBptItem(balancesIncludingBpt); + if (request.tokenIn == IERC20(this)) { - return _onSwapBptGivenTokenOut(request.amount, _skipBptIndex(indexOut), balances); + return _onSwapBptGivenTokenOut(request.amount, _skipBptIndex(indexOut), virtualSupply, balances); } else if (request.tokenOut == IERC20(this)) { - return _onSwapTokenGivenBptOut(request.amount, _skipBptIndex(indexIn), balances); + return _onSwapTokenGivenBptOut(request.amount, _skipBptIndex(indexIn), virtualSupply, balances); } else { return super._onSwapGivenOut(request, balances, _skipBptIndex(indexIn), _skipBptIndex(indexOut)); } @@ -174,12 +179,13 @@ contract StablePhantomPool is StablePool { function _onSwapTokenGivenBptIn( uint256 bptIn, uint256 tokenIndex, + uint256 virtualSupply, uint256[] memory balances ) internal view returns (uint256) { // TODO: calc due protocol fees - uint256 swapFee = getSwapFeePercentage(); - (uint256 currentAmp, ) = _getAmplificationParameter(); - return StableMath._calcTokenOutGivenExactBptIn(currentAmp, balances, tokenIndex, bptIn, totalSupply(), swapFee); + // Use virtual total supply and zero swap fees for joins. + (uint256 amp, ) = _getAmplificationParameter(); + return StableMath._calcTokenOutGivenExactBptIn(amp, balances, tokenIndex, bptIn, virtualSupply, 0); } /** @@ -188,13 +194,13 @@ contract StablePhantomPool is StablePool { function _onSwapTokenGivenBptOut( uint256 bptOut, uint256 tokenIndex, + uint256 virtualSupply, uint256[] memory balances ) internal view returns (uint256) { // TODO: calc due protocol fees - uint256 swapFee = getSwapFeePercentage(); - (uint256 currentAmp, ) = _getAmplificationParameter(); - return - StableMath._calcTokenInGivenExactBptOut(currentAmp, balances, tokenIndex, bptOut, totalSupply(), swapFee); + // Use virtual total supply and zero swap fees for joins + (uint256 amp, ) = _getAmplificationParameter(); + return StableMath._calcTokenInGivenExactBptOut(amp, balances, tokenIndex, bptOut, virtualSupply, 0); } /** @@ -203,20 +209,15 @@ contract StablePhantomPool is StablePool { function _onSwapBptGivenTokenOut( uint256 amountOut, uint256 tokenIndex, + uint256 virtualSupply, uint256[] memory balances ) internal view returns (uint256) { // TODO: calc due protocol fees - (uint256 currentAmp, ) = _getAmplificationParameter(); - uint256[] memory amountsOut = new uint256[](_getTotalTokens() - 1); // Avoid BPT balance for stable pool math + // Avoid BPT balance for stable pool math. Use virtual total supply and zero swap fees for exits. + (uint256 amp, ) = _getAmplificationParameter(); + uint256[] memory amountsOut = new uint256[](_getTotalTokens() - 1); amountsOut[tokenIndex] = amountOut; - return - StableMath._calcBptInGivenExactTokensOut( - currentAmp, - balances, - amountsOut, - totalSupply(), - getSwapFeePercentage() - ); + return StableMath._calcBptInGivenExactTokensOut(amp, balances, amountsOut, virtualSupply, 0); } /** @@ -225,20 +226,15 @@ contract StablePhantomPool is StablePool { function _onSwapBptGivenTokenIn( uint256 amountIn, uint256 tokenIndex, + uint256 virtualSupply, uint256[] memory balances ) internal view returns (uint256) { // TODO: calc due protocol fees - uint256[] memory amountsIn = new uint256[](_getTotalTokens() - 1); // Avoid BPT balance for stable pool math + // Avoid BPT balance for stable pool math. Use virtual total supply and zero swap fees for exits. + uint256[] memory amountsIn = new uint256[](_getTotalTokens() - 1); amountsIn[tokenIndex] = amountIn; - (uint256 currentAmp, ) = _getAmplificationParameter(); - return - StableMath._calcBptOutGivenExactTokensIn( - currentAmp, - balances, - amountsIn, - totalSupply(), - getSwapFeePercentage() - ); + (uint256 amp, ) = _getAmplificationParameter(); + return StableMath._calcBptOutGivenExactTokensIn(amp, balances, amountsIn, virtualSupply, 0); } /** @@ -258,23 +254,24 @@ contract StablePhantomPool is StablePool { StablePool.JoinKind kind = userData.joinKind(); _require(kind == StablePool.JoinKind.INIT, Errors.UNINITIALIZED); - uint256[] memory amountsIn = userData.initialAmountsIn(); - InputHelpers.ensureInputLengthMatch(amountsIn.length, _getTotalTokens()); - _upscaleArray(amountsIn, scalingFactors); + uint256[] memory amountsInIncludingBpt = userData.initialAmountsIn(); + InputHelpers.ensureInputLengthMatch(amountsInIncludingBpt.length, _getTotalTokens()); + _upscaleArray(amountsInIncludingBpt, scalingFactors); - (uint256 currentAmp, ) = _getAmplificationParameter(); - uint256 invariantAfterJoin = StableMath._calculateInvariant(currentAmp, _dropBptItem(amountsIn), true); + (uint256 amp, ) = _getAmplificationParameter(); + (, uint256[] memory amountsIn) = _dropBptItem(amountsInIncludingBpt); + uint256 invariantAfterJoin = StableMath._calculateInvariant(amp, amountsIn, true); // Set the initial BPT to the value of the invariant uint256 bptAmountOut = invariantAfterJoin; - _updateLastInvariant(invariantAfterJoin, currentAmp); + _updateLastInvariant(invariantAfterJoin, amp); // Mint the total amount of BPT to the sender forcing the Vault to pull it uint256 initialBpt = _MAX_TOKEN_BALANCE.sub(bptAmountOut); _mintPoolTokens(sender, initialBpt); _approve(sender, address(getVault()), initialBpt); - amountsIn[_bptIndex] = initialBpt; - return (bptAmountOut, amountsIn); + amountsInIncludingBpt[_bptIndex] = initialBpt; + return (bptAmountOut, amountsInIncludingBpt); } /** @@ -486,7 +483,12 @@ contract StablePhantomPool is StablePool { return index < _bptIndex ? index : index.sub(1); } - function _dropBptItem(uint256[] memory _balances) internal view returns (uint256[] memory balances) { + function _dropBptItem(uint256[] memory _balances) + internal + view + returns (uint256 virtualSupply, uint256[] memory balances) + { + virtualSupply = _MAX_TOKEN_BALANCE - _balances[_bptIndex]; balances = new uint256[](_balances.length - 1); for (uint256 i = 0; i < balances.length; i++) { balances[i] = _balances[i < _bptIndex ? i : i + 1]; diff --git a/pkg/pool-stable-phantom/test/StablePhantomPool.test.ts b/pkg/pool-stable-phantom/test/StablePhantomPool.test.ts index 95660ec8fb..d525c02469 100644 --- a/pkg/pool-stable-phantom/test/StablePhantomPool.test.ts +++ b/pkg/pool-stable-phantom/test/StablePhantomPool.test.ts @@ -16,10 +16,10 @@ import Token from '@balancer-labs/v2-helpers/src/models/tokens/Token'; import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; describe('StablePhantomPool', () => { - let owner: SignerWithAddress, recipient: SignerWithAddress; + let lp: SignerWithAddress, owner: SignerWithAddress, recipient: SignerWithAddress; sharedBeforeEach('setup signers', async () => { - [, owner, recipient] = await ethers.getSigners(); + [, lp, owner, recipient] = await ethers.getSigners(); }); context('for 2 tokens pool', () => { @@ -45,38 +45,35 @@ describe('StablePhantomPool', () => { }); function itBehavesAsStablePhantomPool(numberOfTokens: number): void { - let pool: StablePhantomPool, tokens: TokenList, deployedAt: BigNumber, bptIndex: number; + let pool: StablePhantomPool, tokens: TokenList; + let deployedAt: BigNumber, bptIndex: number, initialBalances: BigNumberish[]; const rateProviders: Contract[] = []; - const tokenRates: BigNumberish[] = []; const priceRateCacheDurations: BigNumberish[] = []; - async function deployPool(params: RawStablePhantomPoolDeployment = {}): Promise { + async function deployPool(params: RawStablePhantomPoolDeployment = {}, rates: BigNumberish[] = []): Promise { tokens = await TokenList.create(numberOfTokens, { sorted: true }); for (let i = 0; i < numberOfTokens; i++) { - tokenRates[i] = fp(1 + (i + 1) / 10); rateProviders[i] = await deploy('v2-pool-utils/MockRateProvider'); - await rateProviders[i].mockRate(tokenRates[i]); + await rateProviders[i].mockRate(rates[i] || fp(1)); priceRateCacheDurations[i] = MONTH + i; } pool = await StablePhantomPool.create({ tokens, rateProviders, priceRateCacheDurations, ...params }); bptIndex = await pool.getBptIndex(); deployedAt = await currentTimestamp(); + initialBalances = Array.from({ length: numberOfTokens + 1 }).map((_, i) => (i == bptIndex ? 0 : fp(1 - i / 10))); } describe('creation', () => { context('when the creation succeeds', () => { - const SWAP_FEE_PERCENTAGE = fp(0.1); - const AMPLIFICATION_PARAMETER = bn(200); + const swapFeePercentage = fp(0.1); + const amplificationParameter = bn(200); + const tokenRates = Array.from({ length: numberOfTokens }, (_, i) => fp(1 + (i + 1) / 10)); sharedBeforeEach('deploy pool', async () => { - await deployPool({ - owner, - swapFeePercentage: SWAP_FEE_PERCENTAGE, - amplificationParameter: AMPLIFICATION_PARAMETER, - }); + await deployPool({ owner, swapFeePercentage, amplificationParameter }, tokenRates); }); it('sets the name', async () => { @@ -122,12 +119,12 @@ describe('StablePhantomPool', () => { it('sets amplification', async () => { const { value, isUpdating, precision } = await pool.getAmplificationParameter(); - expect(value).to.be.equal(AMPLIFICATION_PARAMETER.mul(precision)); + expect(value).to.be.equal(amplificationParameter.mul(precision)); expect(isUpdating).to.be.false; }); it('sets swap fee', async () => { - expect(await pool.getSwapFeePercentage()).to.equal(SWAP_FEE_PERCENTAGE); + expect(await pool.getSwapFeePercentage()).to.equal(swapFeePercentage); }); it('sets the rate providers', async () => { @@ -213,13 +210,8 @@ describe('StablePhantomPool', () => { }); describe('initialize', () => { - let initialBalances: BigNumberish[] = []; - sharedBeforeEach('deploy pool', async () => { await deployPool(); - initialBalances = Array.from({ length: numberOfTokens + 1 }, (_, i) => (i == bptIndex ? 0 : fp(1 - i / 10))); - await tokens.mint({ to: owner, amount: fp(10) }); - await tokens.approve({ from: owner, to: pool.vault, amount: fp(10) }); }); context('when not initialized', () => { @@ -227,18 +219,18 @@ describe('StablePhantomPool', () => { it('transfers the initial balances to the vault', async () => { const previousBalances = await tokens.balanceOf(pool.vault); - await pool.init({ initialBalances, from: owner }); + await pool.init({ initialBalances }); const currentBalances = await tokens.balanceOf(pool.vault); currentBalances.forEach((currentBalance, i) => { - const initialBalanceIndex = i < bptIndex ? i : i + 1; + const initialBalanceIndex = i < bptIndex ? i : i + 1; // initial balances includes BPT const expectedBalance = previousBalances[i].add(initialBalances[initialBalanceIndex]); expect(currentBalance).to.be.equal(expectedBalance); }); }); it('mints the max amount of BPT', async () => { - await pool.init({ initialBalances, from: owner }); + await pool.init({ initialBalances }); expect(await pool.totalSupply()).to.be.equal(MAX_UINT112); }); @@ -246,7 +238,7 @@ describe('StablePhantomPool', () => { it('mints the minimum BPT to the address zero', async () => { const minimumBpt = await pool.instance.getMinimumBpt(); - await pool.init({ initialBalances, from: owner }); + await pool.init({ recipient, initialBalances }); expect(await pool.balanceOf(ZERO_ADDRESS)).to.be.equal(minimumBpt); }); @@ -254,24 +246,25 @@ describe('StablePhantomPool', () => { it('mints the invariant amount of BPT to the recipient', async () => { const invariant = await pool.estimateInvariant(initialBalances); - await pool.init({ recipient, initialBalances, from: owner }); + await pool.init({ recipient, initialBalances, from: lp }); - expect(await pool.balanceOf(recipient)).to.be.equalWithError(invariant, 0.4); + expect(await pool.balanceOf(lp)).to.be.zero; + expect(await pool.balanceOf(recipient)).to.be.equalWithError(invariant, 0.00001); }); it('mints the rest of the BPT to the vault', async () => { const invariant = await pool.estimateInvariant(initialBalances); const minimumBpt = await pool.instance.getMinimumBpt(); - const { amountsIn, dueProtocolFeeAmounts } = await pool.init({ recipient, initialBalances, from: owner }); + const { amountsIn, dueProtocolFeeAmounts } = await pool.init({ initialBalances }); const expectedBPT = MAX_UINT112.sub(minimumBpt).sub(invariant); - expect(await pool.balanceOf(pool.vault)).to.be.equalWithError(expectedBPT, 0.0001); + expect(await pool.balanceOf(pool.vault)).to.be.equalWithError(expectedBPT, 0.00001); expect(dueProtocolFeeAmounts).to.be.zeros; for (let i = 0; i < amountsIn.length; i++) { i === bptIndex - ? expect(amountsIn[i]).to.be.equalWithError(MAX_UINT112.sub(invariant), 0.0001) + ? expect(amountsIn[i]).to.be.equalWithError(MAX_UINT112.sub(invariant), 0.00001) : expect(amountsIn[i]).to.be.equal(initialBalances[i]); } }); @@ -290,13 +283,194 @@ describe('StablePhantomPool', () => { context('when it was already initialized', () => { sharedBeforeEach('init pool', async () => { - await pool.init({ initialBalances, from: owner }); + await pool.init({ initialBalances }); + }); + + it('reverts', async () => { + await expect(pool.init({ initialBalances })).to.be.revertedWith('UNHANDLED_BY_PHANTOM_POOL'); + }); + }); + }); + + describe('swap', () => { + sharedBeforeEach('deploy pool', async () => { + await deployPool(); + }); + + context('when the pool was not initialized', () => { + it('reverts', async () => { + const tx = pool.swapGivenIn({ in: tokens.first, out: tokens.second, amount: fp(1), recipient }); + await expect(tx).to.be.revertedWith('UNINITIALIZED'); + }); + }); + + context('when the pool was initialized', () => { + sharedBeforeEach('initialize pool', async () => { + bptIndex = await pool.getBptIndex(); + const sender = (await ethers.getSigners())[0]; + await pool.init({ initialBalances, recipient: sender }); + }); + + sharedBeforeEach('allow vault', async () => { + const sender = (await ethers.getSigners())[0]; + await tokens.mint({ to: sender, amount: fp(100) }); + await tokens.approve({ from: sender, to: pool.vault }); + await pool.bpt.approve(pool.vault, MAX_UINT112, { from: sender }); + }); + + context('token out given token in', () => { + const amountIn = fp(0.1); + + it('swaps tokens', async () => { + const tokenIn = tokens.first; + const tokenOut = tokens.second; + + const previousBalance = await tokenOut.balanceOf(recipient); + const expectedAmountOut = await pool.estimateTokenOutGivenTokenIn(tokenIn, tokenOut, amountIn); + + const amountOut = await pool.swapGivenIn({ in: tokenIn, out: tokenOut, amount: amountIn, recipient }); + expect(amountOut).to.be.equalWithError(expectedAmountOut, 0.00001); + + const currentBalance = await tokenOut.balanceOf(recipient); + expect(currentBalance.sub(previousBalance)).to.be.equalWithError(expectedAmountOut, 0.00001); + }); + }); + + context('token in given token out', () => { + const amountOut = fp(0.1); + + it('swaps tokens', async () => { + const tokenIn = tokens.first; + const tokenOut = tokens.second; + + const previousBalance = await tokenOut.balanceOf(recipient); + const expectedAmountIn = await pool.estimateTokenInGivenTokenOut(tokenIn, tokenOut, amountOut); + + const amountIn = await pool.swapGivenOut({ in: tokenIn, out: tokenOut, amount: amountOut, recipient }); + expect(amountIn).to.be.equalWithError(expectedAmountIn, 0.00001); + + const currentBalance = await tokenOut.balanceOf(recipient); + expect(currentBalance.sub(previousBalance)).to.be.equal(amountOut); + }); + }); + + context('token out given BPT in', () => { + const bptIn = fp(1); + + it('swaps exact BPT for token', async () => { + const tokenOut = tokens.first; + + const previousBalance = await tokenOut.balanceOf(recipient); + const expectedTokenOut = await pool.estimateTokenOutGivenBptIn(tokenOut, bptIn); + + const amountOut = await pool.swapGivenIn({ in: pool.bpt, out: tokenOut, amount: bptIn, recipient }); + expect(amountOut).to.be.equalWithError(expectedTokenOut, 0.00001); + + const currentBalance = await tokenOut.balanceOf(recipient); + expect(currentBalance.sub(previousBalance)).to.be.equalWithError(expectedTokenOut, 0.00001); + }); + }); + + context('token in given BPT out', () => { + const bptOut = fp(1); + + it('swaps token for exact BPT', async () => { + const tokenIn = tokens.first; + + const previousBalance = await pool.balanceOf(recipient); + const expectedTokenIn = await pool.estimateTokenInGivenBptOut(tokenIn, bptOut); + + const amountIn = await pool.swapGivenOut({ in: tokenIn, out: pool.bpt, amount: bptOut, recipient }); + expect(amountIn).to.be.equalWithError(expectedTokenIn, 0.00001); + + const currentBalance = await pool.balanceOf(recipient); + expect(currentBalance.sub(previousBalance)).to.be.equal(bptOut); + }); + }); + + context('BPT out given token in', () => { + const amountIn = fp(1); + + it('swaps exact token for BPT', async () => { + const tokenIn = tokens.first; + + const previousBalance = await pool.balanceOf(recipient); + const expectedBptOut = await pool.estimateBptOutGivenTokenIn(tokenIn, amountIn); + + const amountOut = await pool.swapGivenIn({ in: tokenIn, out: pool.bpt, amount: amountIn, recipient }); + expect(amountOut).to.be.equalWithError(expectedBptOut, 0.00001); + + const currentBalance = await pool.balanceOf(recipient); + expect(currentBalance.sub(previousBalance)).to.be.equalWithError(expectedBptOut, 0.00001); + }); }); + context('BPT in given token out', () => { + const amountOut = fp(0.1); + + it('swaps BPT for exact tokens', async () => { + const tokenOut = tokens.first; + + const previousBalance = await tokenOut.balanceOf(recipient); + const expectedBptIn = await pool.estimateBptInGivenTokenOut(tokenOut, amountOut); + + const amountIn = await pool.swapGivenOut({ in: pool.bpt, out: tokenOut, amount: amountOut, recipient }); + expect(amountIn).to.be.equalWithError(expectedBptIn, 0.00001); + + const currentBalance = await tokenOut.balanceOf(recipient); + expect(currentBalance.sub(previousBalance)).to.be.equal(amountOut); + }); + }); + }); + }); + + describe('join', () => { + sharedBeforeEach('deploy pool', async () => { + await deployPool(); + await pool.init({ recipient, initialBalances }); + }); + + context('when the sender is the vault', () => { + it('reverts', async () => { + const allTokens = await pool.getTokens(); + const tx = pool.vault.joinPool({ poolId: pool.poolId, tokens: allTokens.tokens, from: lp }); + await expect(tx).to.be.revertedWith('UNHANDLED_BY_PHANTOM_POOL'); + }); + }); + + context('when the sender is not the vault', () => { it('reverts', async () => { - await expect(pool.init({ initialBalances, from: owner })).to.be.revertedWith('UNHANDLED_BY_PHANTOM_POOL'); + const tx = pool.instance.onJoinPool(pool.poolId, ZERO_ADDRESS, ZERO_ADDRESS, [0], 0, 0, '0x'); + await expect(tx).to.be.revertedWith('CALLER_NOT_VAULT'); }); }); }); + + describe('exit', () => { + sharedBeforeEach('deploy pool', async () => { + await deployPool(); + await pool.init({ recipient, initialBalances }); + }); + + context('when the sender is the vault', () => { + it('reverts', async () => { + const allTokens = await pool.getTokens(); + const tx = pool.vault.exitPool({ poolId: pool.poolId, tokens: allTokens.tokens }); + await expect(tx).to.be.revertedWith('UNHANDLED_BY_PHANTOM_POOL'); + }); + }); + + context('when the sender is not the vault', () => { + it('reverts', async () => { + const tx = pool.instance.onExitPool(pool.poolId, ZERO_ADDRESS, ZERO_ADDRESS, [0], 0, 0, '0x'); + await expect(tx).to.be.revertedWith('CALLER_NOT_VAULT'); + }); + }); + }); + + describe('rates cache', () => { + // TODO: implement + // const tokenRates = Array.from({ length: numberOfTokens }, (_, i) => fp(1 + (i + 1) / 10)); + }); } }); diff --git a/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPool.ts b/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPool.ts index 86bbdfc27d..a506f3c167 100644 --- a/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPool.ts +++ b/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPool.ts @@ -1,29 +1,41 @@ +import { ethers } from 'hardhat'; import { BigNumber, Contract } from 'ethers'; + import { SwapKind } from '@balancer-labs/balancer-js'; -import { BigNumberish } from '@balancer-labs/v2-helpers/src/numbers'; +import { BigNumberish, bn } from '@balancer-labs/v2-helpers/src/numbers'; +import { StablePoolEncoder } from '@balancer-labs/balancer-js/src'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { Account } from '../../types/types'; -import { ZERO_ADDRESS } from '../../../constants'; +import { MAX_UINT112, ZERO_ADDRESS } from '../../../constants'; import { GeneralSwap } from '../../vault/types'; import { RawStablePhantomPoolDeployment, SwapPhantomPool } from './types'; import Vault from '../../vault/Vault'; +import Token from '../../tokens/Token'; import TokenList from '../../tokens/TokenList'; import TypesConverter from '../../types/TypesConverter'; import StablePhantomPoolDeployer from './StablePhantomPoolDeployer'; import * as expectEvent from '../../../test/expectEvent'; -import { calculateInvariant } from '../stable/math'; -import { InitStablePool, JoinExitStablePool, JoinResult } from '../stable/types'; -import { StablePoolEncoder } from '@balancer-labs/balancer-js/src'; import { actionId } from '../../misc/actions'; +import { InitStablePool, JoinExitStablePool, JoinResult } from '../stable/types'; +import { + calcBptInGivenExactTokensOut, + calcBptOutGivenExactTokensIn, + calcInGivenOut, + calcOutGivenIn, + calcTokenInGivenExactBptOut, + calcTokenOutGivenExactBptIn, + calculateInvariant, +} from '../stable/math'; export default class StablePhantomPool { instance: Contract; poolId: string; vault: Vault; tokens: TokenList; + bptIndex: number; swapFeePercentage: BigNumberish; amplificationParameter: BigNumberish; owner?: SignerWithAddress; @@ -37,6 +49,7 @@ export default class StablePhantomPool { poolId: string, vault: Vault, tokens: TokenList, + bptIndex: BigNumber, swapFeePercentage: BigNumberish, amplificationParameter: BigNumberish, owner?: SignerWithAddress @@ -45,6 +58,7 @@ export default class StablePhantomPool { this.poolId = poolId; this.vault = vault; this.tokens = tokens; + this.bptIndex = bptIndex.toNumber(); this.swapFeePercentage = swapFeePercentage; this.amplificationParameter = amplificationParameter; this.owner = owner; @@ -54,6 +68,10 @@ export default class StablePhantomPool { return this.instance.address; } + get bpt(): Token { + return new Token('BPT', 'BPT', 18, this.instance); + } + async name(): Promise { return this.instance.name(); } @@ -70,6 +88,10 @@ export default class StablePhantomPool { return this.instance.totalSupply(); } + async virtualTotalSupply(): Promise { + return MAX_UINT112.sub((await this.getBalances())[this.bptIndex]); + } + async balanceOf(account: Account): Promise { return this.instance.balanceOf(TypesConverter.toAddress(account)); } @@ -82,6 +104,10 @@ export default class StablePhantomPool { return this.vault.getPoolTokens(this.poolId); } + async getTokenIndex(token: Token): Promise { + return (await this.getTokens()).tokens.indexOf(token.address); + } + async getBalances(): Promise { return (await this.getTokens()).balances; } @@ -133,21 +159,96 @@ export default class StablePhantomPool { return calculateInvariant(await this._dropBptItem(currentBalances), this.amplificationParameter); } + async estimateTokenOutGivenTokenIn(tokenIn: Token, tokenOut: Token, amountIn: BigNumberish): Promise { + const indexIn = this._skipBptIndex(await this.getTokenIndex(tokenIn)); + const indexOut = this._skipBptIndex(await this.getTokenIndex(tokenOut)); + const currentBalances = await this._dropBptItem(await this.getBalances()); + return bn(calcOutGivenIn(currentBalances, this.amplificationParameter, indexIn, indexOut, amountIn)); + } + + async estimateTokenInGivenTokenOut(tokenIn: Token, tokenOut: Token, amountOut: BigNumberish): Promise { + const indexIn = this._skipBptIndex(await this.getTokenIndex(tokenIn)); + const indexOut = this._skipBptIndex(await this.getTokenIndex(tokenOut)); + const currentBalances = await this._dropBptItem(await this.getBalances()); + return bn(calcInGivenOut(currentBalances, this.amplificationParameter, indexIn, indexOut, amountOut)); + } + + async estimateTokenOutGivenBptIn(token: Token, bptIn: BigNumberish): Promise { + const tokenIndex = this._skipBptIndex(await this.getTokenIndex(token)); + const virtualSupply = await this.virtualTotalSupply(); + const currentBalances = await this._dropBptItem(await this.getBalances()); + + return calcTokenOutGivenExactBptIn( + tokenIndex, + currentBalances, + this.amplificationParameter, + bptIn, + virtualSupply, + 0 + ); + } + + async estimateTokenInGivenBptOut(token: Token, bptOut: BigNumberish): Promise { + const tokenIndex = this._skipBptIndex(await this.getTokenIndex(token)); + const virtualSupply = await this.virtualTotalSupply(); + const currentBalances = await this._dropBptItem(await this.getBalances()); + + return calcTokenInGivenExactBptOut( + tokenIndex, + currentBalances, + this.amplificationParameter, + bptOut, + virtualSupply, + 0 + ); + } + + async estimateBptOutGivenTokenIn(token: Token, amountIn: BigNumberish): Promise { + const tokenIndex = this._skipBptIndex(await this.getTokenIndex(token)); + const virtualSupply = await this.virtualTotalSupply(); + const currentBalances = await this._dropBptItem(await this.getBalances()); + const amountsIn = Array.from({ length: currentBalances.length }, (_, i) => (i == tokenIndex ? amountIn : 0)); + + return calcBptOutGivenExactTokensIn(currentBalances, this.amplificationParameter, amountsIn, virtualSupply, 0); + } + + async estimateBptInGivenTokenOut(token: Token, amountOut: BigNumberish): Promise { + const tokenIndex = this._skipBptIndex(await this.getTokenIndex(token)); + const virtualSupply = await this.virtualTotalSupply(); + const currentBalances = await this._dropBptItem(await this.getBalances()); + const amountsOut = Array.from({ length: currentBalances.length }, (_, i) => (i == tokenIndex ? amountOut : 0)); + + return calcBptInGivenExactTokensOut(currentBalances, this.amplificationParameter, amountsOut, virtualSupply, 0); + } + async swapGivenIn(params: SwapPhantomPool): Promise { - return this.swap(await this._buildSwapParams(SwapKind.GivenIn, params)); + const { amountOut } = await this.swap(await this._buildSwapParams(SwapKind.GivenIn, params)); + return amountOut; } async swapGivenOut(params: SwapPhantomPool): Promise { - return this.swap(await this._buildSwapParams(SwapKind.GivenOut, params)); + const { amountIn } = await this.swap(await this._buildSwapParams(SwapKind.GivenOut, params)); + return amountIn; } - async swap(params: GeneralSwap): Promise { + async swap(params: GeneralSwap): Promise<{ amountIn: BigNumber; amountOut: BigNumber }> { const tx = await this.vault.generalSwap(params); - const { amount } = expectEvent.inReceipt(await tx.wait(), 'Swap').args; - return amount; + return expectEvent.inReceipt(await tx.wait(), 'Swap').args; } async init(initParams: InitStablePool): Promise { + const from = initParams.from || (await ethers.getSigners())[0]; + const initialBalances = initParams.initialBalances; + const balances = await this._dropBptItem(Array.isArray(initialBalances) ? initialBalances : [initialBalances]); + + await Promise.all( + balances.map(async (balance, i) => { + const token = this.tokens.get(i); + await token.mint(from, balance); + await token.approve(this.vault, balance, { from }); + }) + ); + const { tokens: allTokens } = await this.getTokens(); const params: JoinExitStablePool = this._buildInitParams(initParams); const currentBalances = params.currentBalances || (await this.getBalances()); @@ -171,21 +272,20 @@ export default class StablePhantomPool { } private async _buildSwapParams(kind: number, params: SwapPhantomPool): Promise { - const { tokens: allTokens } = await this.getTokens(); return { kind, poolAddress: this.address, poolId: this.poolId, from: params.from, to: TypesConverter.toAddress(params.recipient), - tokenIn: params.in < allTokens.length ? allTokens[params.in] : ZERO_ADDRESS, - tokenOut: params.out < allTokens.length ? allTokens[params.out] : ZERO_ADDRESS, + tokenIn: params.in.address || ZERO_ADDRESS, + tokenOut: params.out.address || ZERO_ADDRESS, lastChangeBlock: params.lastChangeBlock ?? 0, data: params.data ?? '0x', amount: params.amount, - balances: params.balances, - indexIn: params.in, - indexOut: params.out, + balances: params.balances || (await this.getTokens()).balances, + indexIn: await this.getTokenIndex(params.in), + indexOut: await this.getTokenIndex(params.out), }; } @@ -201,10 +301,13 @@ export default class StablePhantomPool { }; } + private _skipBptIndex(index: number): number { + return index < this.bptIndex ? index : index - 1; + } + private async _dropBptItem(items: BigNumberish[]): Promise { - const bptIndex = await this.getBptIndex(); const result = []; - for (let i = 0; i < items.length - 1; i++) result[i] = items[i < bptIndex ? i : i + 1]; + for (let i = 0; i < items.length - 1; i++) result[i] = items[i < this.bptIndex ? i : i + 1]; return result; } } diff --git a/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPoolDeployer.ts b/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPoolDeployer.ts index 4054a75d9c..1f9c87009a 100644 --- a/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPoolDeployer.ts +++ b/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPoolDeployer.ts @@ -19,8 +19,19 @@ export default { const pool = await this._deployStandalone(deployment, vault); const poolId = await pool.getPoolId(); + const bptIndex = await pool.getBptIndex(); const { tokens, swapFeePercentage, amplificationParameter, owner } = deployment; - return new StablePhantomPool(pool, poolId, vault, tokens, swapFeePercentage, amplificationParameter, owner); + + return new StablePhantomPool( + pool, + poolId, + vault, + tokens, + bptIndex, + swapFeePercentage, + amplificationParameter, + owner + ); }, async _deployStandalone(params: StablePhantomPoolDeployment, vault: Vault): Promise { @@ -37,7 +48,7 @@ export default { const owner = TypesConverter.toAddress(params.owner); - const pool = await deploy('v2-pool-stable-phantom/StablePhantomPool', { + return deploy('v2-pool-stable-phantom/StablePhantomPool', { args: [ { vault: vault.address, @@ -55,8 +66,5 @@ export default { ], from, }); - - // await pool.initialize(); - return pool; }, }; diff --git a/pvt/helpers/src/models/pools/stable-phantom/types.ts b/pvt/helpers/src/models/pools/stable-phantom/types.ts index 94cee7a4c5..b8c0bee1cc 100644 --- a/pvt/helpers/src/models/pools/stable-phantom/types.ts +++ b/pvt/helpers/src/models/pools/stable-phantom/types.ts @@ -3,6 +3,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-wit import { BigNumberish } from '../../../numbers'; import Vault from '../../vault/Vault'; +import Token from '../../tokens/Token'; import TokenList from '../../tokens/TokenList'; import { Account } from '../../types/types'; @@ -35,10 +36,10 @@ export type StablePhantomPoolDeployment = { }; export type SwapPhantomPool = { - in: number; - out: number; + in: Token; + out: Token; amount: BigNumberish; - balances: BigNumberish[]; + balances?: BigNumberish[]; recipient?: Account; from?: SignerWithAddress; lastChangeBlock?: BigNumberish; diff --git a/pvt/helpers/src/models/pools/stable/math.ts b/pvt/helpers/src/models/pools/stable/math.ts index 65dfd90563..e31e064500 100644 --- a/pvt/helpers/src/models/pools/stable/math.ts +++ b/pvt/helpers/src/models/pools/stable/math.ts @@ -224,11 +224,11 @@ export function calcTokenInGivenExactBptOut( } export function calcBptInGivenExactTokensOut( - fpBalances: BigNumber[], + fpBalances: BigNumberish[], amplificationParameter: BigNumberish, - fpAmountsOut: BigNumber[], - fpBptTotalSupply: BigNumber, - fpSwapFeePercentage: BigNumber + fpAmountsOut: BigNumberish[], + fpBptTotalSupply: BigNumberish, + fpSwapFeePercentage: BigNumberish ): BigNumber { // Get current invariant const currentInvariant = fromFp(calculateInvariant(fpBalances, amplificationParameter)); diff --git a/pvt/helpers/src/models/vault/Vault.ts b/pvt/helpers/src/models/vault/Vault.ts index 83af9f2351..d1de8b0424 100644 --- a/pvt/helpers/src/models/vault/Vault.ts +++ b/pvt/helpers/src/models/vault/Vault.ts @@ -1,4 +1,5 @@ import { ethers } from 'hardhat'; +import { SwapKind } from '@balancer-labs/balancer-js'; import { BigNumber, Contract, ContractTransaction } from 'ethers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; @@ -7,11 +8,11 @@ import TokenList from '../tokens/TokenList'; import VaultDeployer from './VaultDeployer'; import TypesConverter from '../types/TypesConverter'; import { actionId } from '../misc/actions'; -import { MAX_UINT256, ZERO_ADDRESS } from '../../constants'; +import { deployedAt } from '../../contract'; import { BigNumberish } from '../../numbers'; import { Account, NAry, TxParams } from '../types/types'; +import { MAX_UINT256, ZERO_ADDRESS } from '../../constants'; import { ExitPool, JoinPool, RawVaultDeployment, MinimalSwap, GeneralSwap } from './types'; -import { deployedAt } from '../../contract'; export default class Vault { mocked: boolean; @@ -77,23 +78,45 @@ export default class Vault { } async generalSwap(params: GeneralSwap): Promise { - return this.instance.callGeneralPoolSwap( - params.poolAddress, - { - kind: params.kind, - poolId: params.poolId, - from: params.from ?? ZERO_ADDRESS, - to: params.to, - tokenIn: params.tokenIn, - tokenOut: params.tokenOut, - lastChangeBlock: params.lastChangeBlock, - userData: params.data, - amount: params.amount, - }, - params.balances, - params.indexIn, - params.indexOut - ); + const sender = (params.from || (await this._defaultSender())).address; + const vault = params.from ? this.instance.connect(sender) : this.instance; + + return this.mocked + ? vault.callGeneralPoolSwap( + params.poolAddress, + { + kind: params.kind, + poolId: params.poolId, + from: params.from ?? ZERO_ADDRESS, + to: params.to, + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + lastChangeBlock: params.lastChangeBlock, + userData: params.data, + amount: params.amount, + }, + params.balances, + params.indexIn, + params.indexOut + ) + : vault.swap( + { + poolId: params.poolId, + kind: params.kind, + assetIn: params.tokenIn, + assetOut: params.tokenOut, + amount: params.amount, + userData: params.data, + }, + { + sender: sender, + fromInternalBalance: false, + recipient: TypesConverter.toAddress(params.to), + toInternalBalance: false, + }, + params.kind === SwapKind.GivenIn ? 0 : MAX_UINT256, + MAX_UINT256 + ); } async joinPool(params: JoinPool): Promise {