diff --git a/pkg/pool-stable-phantom/contracts/StablePhantomPool.sol b/pkg/pool-stable-phantom/contracts/StablePhantomPool.sol index f858880c10..4bc03bd1ca 100644 --- a/pkg/pool-stable-phantom/contracts/StablePhantomPool.sol +++ b/pkg/pool-stable-phantom/contracts/StablePhantomPool.sol @@ -15,8 +15,6 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; -import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/EnumerableMap.sol"; - import "@balancer-labs/v2-pool-stable/contracts/StablePool.sol"; import "@balancer-labs/v2-pool-utils/contracts/rates/PriceRateCache.sol"; import "@balancer-labs/v2-pool-utils/contracts/interfaces/IRateProvider.sol"; @@ -25,10 +23,9 @@ import "@balancer-labs/v2-solidity-utils/contracts/helpers/ERC20Helpers.sol"; import "@balancer-labs/v2-solidity-utils/contracts/helpers/BalancerErrors.sol"; contract StablePhantomPool is StablePool { - using WordCodec for bytes32; using FixedPoint for uint256; using PriceRateCache for bytes32; - using EnumerableMap for EnumerableMap.IERC20ToBytes32Map; + using StablePoolUserDataHelpers for bytes; uint256 private constant _MIN_TOKENS = 2; uint256 private constant _MAX_TOKEN_BALANCE = 2**(112) - 1; @@ -100,30 +97,12 @@ contract StablePhantomPool is StablePool { _bptIndex = bptIndex; } - function getMinimumBpt() external view returns (uint256) { + function getMinimumBpt() external pure returns (uint256) { return _getMinimumBpt(); } - /** - * @dev Due to how this pool works, all the BPT needs to be minted initially. Since we cannot do that in the - * constructor because the Vault would call back this contract, this method is provided. This function must always - * be called right after construction, therefore it is extremely recommended to create StablePhantom pools using - * the StablePhantomPoolFactory which already does that automatically. - */ - function initialize() external { - bytes32 poolId = getPoolId(); - (IERC20[] memory tokens, , ) = getVault().getPoolTokens(poolId); - uint256[] memory maxAmountsIn = new uint256[](_getTotalTokens()); - maxAmountsIn[_bptIndex] = _MAX_TOKEN_BALANCE; - - IVault.JoinPoolRequest memory request = IVault.JoinPoolRequest({ - assets: _asIAsset(tokens), - maxAmountsIn: maxAmountsIn, - userData: "", - fromInternalBalance: false - }); - - getVault().joinPool(poolId, address(this), address(this), request); + function getBptIndex() external view returns (uint256) { + return _bptIndex; } /** @@ -263,21 +242,39 @@ contract StablePhantomPool is StablePool { } /** - * @dev Initialize phantom BPT + * @dev Due to how this pool works, all the BPT needs to be minted initially. On one hand, we cannot do that in the + * constructor because the Vault would call back this contract. On the other hand, this pool also requires to be + * initialized with a proportional join due to how the Stable math works. + * Then, the approach followed is to mint the total amount of BPT to the sender initializing the pool so it can + * be fetched by the Vault as part of the initialization process. */ function _onInitializePool( bytes32, + address sender, address, - address, - uint256[] memory, - bytes memory + uint256[] memory scalingFactors, + bytes memory userData ) internal override whenNotPaused returns (uint256, uint256[] memory) { - // Mint initial BPTs and adds them to the Vault via a special join - uint256 initialBPT = _MAX_TOKEN_BALANCE.sub(_getMinimumBpt()); - _approve(address(this), address(getVault()), initialBPT); - uint256[] memory amountsIn = new uint256[](_getTotalTokens()); - amountsIn[_bptIndex] = initialBPT; - return (_MAX_TOKEN_BALANCE, amountsIn); + 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 currentAmp, ) = _getAmplificationParameter(); + uint256 invariantAfterJoin = StableMath._calculateInvariant(currentAmp, _dropBptItem(amountsIn), true); + + // Set the initial BPT to the value of the invariant + uint256 bptAmountOut = invariantAfterJoin; + _updateLastInvariant(invariantAfterJoin, currentAmp); + + // 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); } /** diff --git a/pkg/pool-stable-phantom/contracts/StablePhantomPoolFactory.sol b/pkg/pool-stable-phantom/contracts/StablePhantomPoolFactory.sol index 119e23242a..9b16658342 100644 --- a/pkg/pool-stable-phantom/contracts/StablePhantomPoolFactory.sol +++ b/pkg/pool-stable-phantom/contracts/StablePhantomPoolFactory.sol @@ -41,25 +41,25 @@ contract StablePhantomPoolFactory is BasePoolSplitCodeFactory, FactoryWidePauseW address owner ) external returns (StablePhantomPool) { (uint256 pauseWindowDuration, uint256 bufferPeriodDuration) = getPauseConfiguration(); - address pool = _create( - abi.encode( - StablePhantomPool.NewPoolParams({ - vault: getVault(), - name: name, - symbol: symbol, - tokens: tokens, - rateProviders: rateProviders, - priceRateCacheDurations: priceRateCacheDurations, - amplificationParameter: amplificationParameter, - swapFeePercentage: swapFeePercentage, - pauseWindowDuration: pauseWindowDuration, - bufferPeriodDuration: bufferPeriodDuration, - owner: owner - }) - ) - ); - - StablePhantomPool(pool).initialize(); - return StablePhantomPool(pool); + return + StablePhantomPool( + _create( + abi.encode( + StablePhantomPool.NewPoolParams({ + vault: getVault(), + name: name, + symbol: symbol, + tokens: tokens, + rateProviders: rateProviders, + priceRateCacheDurations: priceRateCacheDurations, + amplificationParameter: amplificationParameter, + swapFeePercentage: swapFeePercentage, + pauseWindowDuration: pauseWindowDuration, + bufferPeriodDuration: bufferPeriodDuration, + owner: owner + }) + ) + ) + ); } } diff --git a/pkg/pool-stable-phantom/test/StablePhantomPool.test.ts b/pkg/pool-stable-phantom/test/StablePhantomPool.test.ts new file mode 100644 index 0000000000..95660ec8fb --- /dev/null +++ b/pkg/pool-stable-phantom/test/StablePhantomPool.test.ts @@ -0,0 +1,302 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { BigNumber, Contract } from 'ethers'; + +import { deploy } from '@balancer-labs/v2-helpers/src/contract'; +import { MAX_UINT112, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; +import { PoolSpecialization } from '@balancer-labs/balancer-js'; +import { BigNumberish, bn, fp } from '@balancer-labs/v2-helpers/src/numbers'; +import { currentTimestamp, MONTH } from '@balancer-labs/v2-helpers/src/time'; +import { RawStablePhantomPoolDeployment } from '@balancer-labs/v2-helpers/src/models/pools/stable-phantom/types'; + +import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; +import StablePhantomPool from '@balancer-labs/v2-helpers/src/models/pools/stable-phantom/StablePhantomPool'; +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; + + sharedBeforeEach('setup signers', async () => { + [, owner, recipient] = await ethers.getSigners(); + }); + + context('for 2 tokens pool', () => { + itBehavesAsStablePhantomPool(2); + }); + + context('for 4 tokens pool', () => { + itBehavesAsStablePhantomPool(4); + }); + + context('for 1 token pool', () => { + it('reverts', async () => { + const tokens = await TokenList.create(1); + await expect(StablePhantomPool.create({ tokens })).to.be.revertedWith('MIN_TOKENS'); + }); + }); + + context('for 5 tokens pool', () => { + it('reverts', async () => { + const tokens = await TokenList.create(5, { sorted: true }); + await expect(StablePhantomPool.create({ tokens })).to.be.revertedWith('MAX_TOKENS'); + }); + }); + + function itBehavesAsStablePhantomPool(numberOfTokens: number): void { + let pool: StablePhantomPool, tokens: TokenList, deployedAt: BigNumber, bptIndex: number; + + const rateProviders: Contract[] = []; + const tokenRates: BigNumberish[] = []; + const priceRateCacheDurations: BigNumberish[] = []; + + async function deployPool(params: RawStablePhantomPoolDeployment = {}): 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]); + priceRateCacheDurations[i] = MONTH + i; + } + + pool = await StablePhantomPool.create({ tokens, rateProviders, priceRateCacheDurations, ...params }); + bptIndex = await pool.getBptIndex(); + deployedAt = await currentTimestamp(); + } + + describe('creation', () => { + context('when the creation succeeds', () => { + const SWAP_FEE_PERCENTAGE = fp(0.1); + const AMPLIFICATION_PARAMETER = bn(200); + + sharedBeforeEach('deploy pool', async () => { + await deployPool({ + owner, + swapFeePercentage: SWAP_FEE_PERCENTAGE, + amplificationParameter: AMPLIFICATION_PARAMETER, + }); + }); + + it('sets the name', async () => { + expect(await pool.name()).to.equal('Balancer Pool Token'); + }); + + it('sets the symbol', async () => { + expect(await pool.symbol()).to.equal('BPT'); + }); + + it('sets the decimals', async () => { + expect(await pool.decimals()).to.equal(18); + }); + + it('sets the owner ', async () => { + expect(await pool.getOwner()).to.equal(owner.address); + }); + + it('sets the vault correctly', async () => { + expect(await pool.getVault()).to.equal(pool.vault.address); + }); + + it('uses general specialization', async () => { + const { address, specialization } = await pool.getRegisteredInfo(); + + expect(address).to.equal(pool.address); + expect(specialization).to.equal(PoolSpecialization.GeneralPool); + }); + + it('registers tokens in the vault', async () => { + const { tokens: poolTokens, balances } = await pool.getTokens(); + + expect(poolTokens).to.have.lengthOf(numberOfTokens + 1); + expect(poolTokens).to.include.members(tokens.addresses); + expect(poolTokens).to.include(pool.address); + expect(balances).to.be.zeros; + }); + + it('starts with no BPT', async () => { + expect(await pool.totalSupply()).to.be.equal(0); + }); + + it('sets amplification', async () => { + const { value, isUpdating, precision } = await pool.getAmplificationParameter(); + + expect(value).to.be.equal(AMPLIFICATION_PARAMETER.mul(precision)); + expect(isUpdating).to.be.false; + }); + + it('sets swap fee', async () => { + expect(await pool.getSwapFeePercentage()).to.equal(SWAP_FEE_PERCENTAGE); + }); + + it('sets the rate providers', async () => { + const providers = await pool.getRateProviders(); + + // BPT does not have a rate provider + expect(providers).to.have.lengthOf(numberOfTokens + 1); + expect(providers).to.include.members(rateProviders.map((r) => r.address)); + expect(providers).to.include(ZERO_ADDRESS); + }); + + it('sets the rate cache durations', async () => { + await tokens.asyncEach(async (token, i) => { + const { duration, expires, rate } = await pool.getPriceRateCache(token); + expect(rate).to.equal(tokenRates[i]); + expect(duration).to.equal(priceRateCacheDurations[i]); + expect(expires).to.be.at.least(deployedAt.add(priceRateCacheDurations[i])); + }); + }); + + it('sets no rate cache duration for BPT', async () => { + const { duration, expires, rate } = await pool.getPriceRateCache(pool.address); + + expect(rate).to.be.zero; + expect(duration).to.be.zero; + expect(expires).to.be.zero; + }); + + it('sets the scaling factors', async () => { + const scalingFactors = (await pool.getScalingFactors()).map((sf) => sf.toString()); + + // It also includes the BPT scaling factor + expect(scalingFactors).to.have.lengthOf(numberOfTokens + 1); + expect(scalingFactors).to.include(fp(1).toString()); + for (const rate of tokenRates) expect(scalingFactors).to.include(rate.toString()); + }); + + it('sets BPT index correctly', async () => { + const bpt = new Token('BPT', 'BPT', 18, pool.instance); + const allTokens = new TokenList([...tokens.tokens, bpt]).sort(); + const expectedIndex = allTokens.indexOf(bpt); + expect(await pool.getBptIndex()).to.be.equal(expectedIndex); + }); + }); + + context('when the creation fails', () => { + it('reverts if there are repeated tokens', async () => { + const badTokens = new TokenList(Array(numberOfTokens).fill(tokens.first)); + + await expect(deployPool({ tokens: badTokens })).to.be.revertedWith('UNSORTED_ARRAY'); + }); + + it('reverts if the cache durations do not match the tokens length', async () => { + const priceRateCacheDurations = [1]; + + await expect(deployPool({ priceRateCacheDurations })).to.be.revertedWith('INPUT_LENGTH_MISMATCH'); + }); + + it('reverts if the rate providers do not match the tokens length', async () => { + const rateProviders = [ZERO_ADDRESS]; + + await expect(deployPool({ rateProviders })).to.be.revertedWith('INPUT_LENGTH_MISMATCH'); + }); + + it('reverts if the swap fee is too high', async () => { + const swapFeePercentage = fp(0.1).add(1); + + await expect(deployPool({ swapFeePercentage })).to.be.revertedWith('MAX_SWAP_FEE_PERCENTAGE'); + }); + + it('reverts if amplification coefficient is too high', async () => { + const amplificationParameter = bn(5001); + + await expect(deployPool({ amplificationParameter })).to.be.revertedWith('MAX_AMP'); + }); + + it('reverts if amplification coefficient is too low', async () => { + const amplificationParameter = bn(0); + + await expect(deployPool({ amplificationParameter })).to.be.revertedWith('MIN_AMP'); + }); + }); + }); + + 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', () => { + context('when not paused', () => { + it('transfers the initial balances to the vault', async () => { + const previousBalances = await tokens.balanceOf(pool.vault); + + await pool.init({ initialBalances, from: owner }); + + const currentBalances = await tokens.balanceOf(pool.vault); + currentBalances.forEach((currentBalance, i) => { + const initialBalanceIndex = i < bptIndex ? i : i + 1; + 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 }); + + expect(await pool.totalSupply()).to.be.equal(MAX_UINT112); + }); + + it('mints the minimum BPT to the address zero', async () => { + const minimumBpt = await pool.instance.getMinimumBpt(); + + await pool.init({ initialBalances, from: owner }); + + expect(await pool.balanceOf(ZERO_ADDRESS)).to.be.equal(minimumBpt); + }); + + it('mints the invariant amount of BPT to the recipient', async () => { + const invariant = await pool.estimateInvariant(initialBalances); + + await pool.init({ recipient, initialBalances, from: owner }); + + expect(await pool.balanceOf(recipient)).to.be.equalWithError(invariant, 0.4); + }); + + 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 expectedBPT = MAX_UINT112.sub(minimumBpt).sub(invariant); + expect(await pool.balanceOf(pool.vault)).to.be.equalWithError(expectedBPT, 0.0001); + + 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.equal(initialBalances[i]); + } + }); + }); + + context('when paused', () => { + sharedBeforeEach('pause pool', async () => { + await pool.pause(); + }); + + it('reverts', async () => { + await expect(pool.init({ initialBalances })).to.be.revertedWith('PAUSED'); + }); + }); + }); + + context('when it was already initialized', () => { + sharedBeforeEach('init pool', async () => { + await pool.init({ initialBalances, from: owner }); + }); + + it('reverts', async () => { + await expect(pool.init({ initialBalances, from: owner })).to.be.revertedWith('UNHANDLED_BY_PHANTOM_POOL'); + }); + }); + }); + } +}); diff --git a/pkg/pool-stable-phantom/test/StablePhantomPoolFactory.test.ts b/pkg/pool-stable-phantom/test/StablePhantomPoolFactory.test.ts index b3536adb62..17cdbd4f0a 100644 --- a/pkg/pool-stable-phantom/test/StablePhantomPoolFactory.test.ts +++ b/pkg/pool-stable-phantom/test/StablePhantomPoolFactory.test.ts @@ -7,7 +7,8 @@ import Vault from '@balancer-labs/v2-helpers/src/models/vault/Vault'; import TokenList from '@balancer-labs/v2-helpers/src/models/tokens/TokenList'; import * as expectEvent from '@balancer-labs/v2-helpers/src/test/expectEvent'; import { fp } from '@balancer-labs/v2-helpers/src/numbers'; -import { MAX_UINT112, ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import { ZERO_ADDRESS } from '@balancer-labs/v2-helpers/src/constants'; +import { sharedBeforeEach } from '@balancer-labs/v2-common/sharedBeforeEach'; import { deploy, deployedAt } from '@balancer-labs/v2-helpers/src/contract'; import { advanceTime, currentTimestamp, MONTH } from '@balancer-labs/v2-helpers/src/time'; @@ -77,15 +78,11 @@ describe('StablePhantomPoolFactory', function () { expect(poolTokens.tokens).to.include(tokens.addresses[1]); expect(poolTokens.tokens).to.include(tokens.addresses[2]); expect(poolTokens.tokens).to.include(pool.address); - - const minimumBPT = await pool.getMinimumBpt(); - poolTokens.tokens.forEach((token, i) => { - expect(poolTokens.balances[i]).to.be.eq(token === pool.address ? MAX_UINT112.sub(minimumBPT) : 0); - }); + expect(poolTokens.balances).to.be.zeros; }); - it('starts with max BPT minted', async () => { - expect(await pool.totalSupply()).to.be.equal(MAX_UINT112); + it('starts with no BPT', async () => { + expect(await pool.totalSupply()).to.be.equal(0); }); it('sets no asset managers', async () => { diff --git a/pkg/pool-stable/contracts/StablePool.sol b/pkg/pool-stable/contracts/StablePool.sol index 50720cba14..2655300aab 100644 --- a/pkg/pool-stable/contracts/StablePool.sol +++ b/pkg/pool-stable/contracts/StablePool.sol @@ -496,7 +496,7 @@ contract StablePool is BaseGeneralPool, BaseMinimalSwapInfoPool, IRateProvider { /** * @dev Stores the last measured invariant, and the amplification parameter used to compute it. */ - function _updateLastInvariant(uint256 invariant, uint256 amplificationParameter) private { + function _updateLastInvariant(uint256 invariant, uint256 amplificationParameter) internal { _lastInvariant = invariant; _lastInvariantAmp = amplificationParameter; } diff --git a/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPool.ts b/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPool.ts new file mode 100644 index 0000000000..86bbdfc27d --- /dev/null +++ b/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPool.ts @@ -0,0 +1,210 @@ +import { BigNumber, Contract } from 'ethers'; +import { SwapKind } from '@balancer-labs/balancer-js'; +import { BigNumberish } from '@balancer-labs/v2-helpers/src/numbers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; + +import { Account } from '../../types/types'; +import { ZERO_ADDRESS } from '../../../constants'; +import { GeneralSwap } from '../../vault/types'; +import { RawStablePhantomPoolDeployment, SwapPhantomPool } from './types'; + +import Vault from '../../vault/Vault'; +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'; + +export default class StablePhantomPool { + instance: Contract; + poolId: string; + vault: Vault; + tokens: TokenList; + swapFeePercentage: BigNumberish; + amplificationParameter: BigNumberish; + owner?: SignerWithAddress; + + static async create(params: RawStablePhantomPoolDeployment = {}): Promise { + return StablePhantomPoolDeployer.deploy(params); + } + + constructor( + instance: Contract, + poolId: string, + vault: Vault, + tokens: TokenList, + swapFeePercentage: BigNumberish, + amplificationParameter: BigNumberish, + owner?: SignerWithAddress + ) { + this.instance = instance; + this.poolId = poolId; + this.vault = vault; + this.tokens = tokens; + this.swapFeePercentage = swapFeePercentage; + this.amplificationParameter = amplificationParameter; + this.owner = owner; + } + + get address(): string { + return this.instance.address; + } + + async name(): Promise { + return this.instance.name(); + } + + async symbol(): Promise { + return this.instance.symbol(); + } + + async decimals(): Promise { + return this.instance.decimals(); + } + + async totalSupply(): Promise { + return this.instance.totalSupply(); + } + + async balanceOf(account: Account): Promise { + return this.instance.balanceOf(TypesConverter.toAddress(account)); + } + + async getRegisteredInfo(): Promise<{ address: string; specialization: BigNumber }> { + return this.vault.getPool(this.poolId); + } + + async getTokens(): Promise<{ tokens: string[]; balances: BigNumber[]; lastChangeBlock: BigNumber }> { + return this.vault.getPoolTokens(this.poolId); + } + + async getBalances(): Promise { + return (await this.getTokens()).balances; + } + + async getVault(): Promise { + return this.instance.getVault(); + } + + async getOwner(): Promise { + return this.instance.getOwner(); + } + + async getPoolId(): Promise { + return this.instance.getPoolId(); + } + + async getSwapFeePercentage(): Promise { + return this.instance.getSwapFeePercentage(); + } + + async getAmplificationParameter(): Promise<{ value: BigNumber; isUpdating: boolean; precision: BigNumber }> { + return this.instance.getAmplificationParameter(); + } + + async getBptIndex(): Promise { + return (await this.instance.getBptIndex()).toNumber(); + } + + async getScalingFactors(): Promise { + return this.instance.getScalingFactors(); + } + + async getRateProviders(): Promise { + return this.instance.getRateProviders(); + } + + async getPriceRateCache(token: Account): Promise<{ expires: BigNumber; rate: BigNumber; duration: BigNumber }> { + return this.instance.getPriceRateCache(typeof token === 'string' ? token : token.address); + } + + async pause(): Promise { + const action = await actionId(this.instance, 'setPaused'); + await this.vault.grantRole(action); + await this.instance.setPaused(true); + } + + async estimateInvariant(currentBalances?: BigNumberish[]): Promise { + if (!currentBalances) currentBalances = await this.getBalances(); + return calculateInvariant(await this._dropBptItem(currentBalances), this.amplificationParameter); + } + + async swapGivenIn(params: SwapPhantomPool): Promise { + return this.swap(await this._buildSwapParams(SwapKind.GivenIn, params)); + } + + async swapGivenOut(params: SwapPhantomPool): Promise { + return this.swap(await this._buildSwapParams(SwapKind.GivenOut, params)); + } + + async swap(params: GeneralSwap): Promise { + const tx = await this.vault.generalSwap(params); + const { amount } = expectEvent.inReceipt(await tx.wait(), 'Swap').args; + return amount; + } + + async init(initParams: InitStablePool): Promise { + const { tokens: allTokens } = await this.getTokens(); + const params: JoinExitStablePool = this._buildInitParams(initParams); + const currentBalances = params.currentBalances || (await this.getBalances()); + const to = params.recipient ? TypesConverter.toAddress(params.recipient) : params.from?.address ?? ZERO_ADDRESS; + + const tx = this.vault.joinPool({ + poolAddress: this.address, + poolId: this.poolId, + recipient: to, + currentBalances, + tokens: allTokens, + lastChangeBlock: params.lastChangeBlock ?? 0, + protocolFeePercentage: params.protocolFeePercentage ?? 0, + data: params.data ?? '0x', + from: params.from, + }); + + const receipt = await (await tx).wait(); + const { deltas, protocolFeeAmounts } = expectEvent.inReceipt(receipt, 'PoolBalanceChanged').args; + return { amountsIn: deltas, dueProtocolFeeAmounts: protocolFeeAmounts }; + } + + 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, + lastChangeBlock: params.lastChangeBlock ?? 0, + data: params.data ?? '0x', + amount: params.amount, + balances: params.balances, + indexIn: params.in, + indexOut: params.out, + }; + } + + private _buildInitParams(params: InitStablePool): JoinExitStablePool { + const { initialBalances: balances } = params; + const amountsIn = Array.isArray(balances) ? balances : Array(this.tokens.length).fill(balances); + + return { + from: params.from, + recipient: params.recipient, + protocolFeePercentage: params.protocolFeePercentage, + data: StablePoolEncoder.joinInit(amountsIn), + }; + } + + 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]; + return result; + } +} diff --git a/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPoolDeployer.ts b/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPoolDeployer.ts new file mode 100644 index 0000000000..4054a75d9c --- /dev/null +++ b/pvt/helpers/src/models/pools/stable-phantom/StablePhantomPoolDeployer.ts @@ -0,0 +1,62 @@ +import { Contract } from 'ethers'; +import { deploy } from '@balancer-labs/v2-helpers/src/contract'; + +import { RawStablePhantomPoolDeployment, StablePhantomPoolDeployment } from './types'; + +import Vault from '../../vault/Vault'; +import VaultDeployer from '../../vault/VaultDeployer'; +import TypesConverter from '../../types/TypesConverter'; +import StablePhantomPool from './StablePhantomPool'; + +const NAME = 'Balancer Pool Token'; +const SYMBOL = 'BPT'; + +export default { + async deploy(params: RawStablePhantomPoolDeployment): Promise { + const deployment = TypesConverter.toStablePhantomPoolDeployment(params); + const vaultParams = { ...TypesConverter.toRawVaultDeployment(params), mocked: params.mockedVault ?? false }; + const vault = params?.vault ?? (await VaultDeployer.deploy(vaultParams)); + const pool = await this._deployStandalone(deployment, vault); + + const poolId = await pool.getPoolId(); + const { tokens, swapFeePercentage, amplificationParameter, owner } = deployment; + return new StablePhantomPool(pool, poolId, vault, tokens, swapFeePercentage, amplificationParameter, owner); + }, + + async _deployStandalone(params: StablePhantomPoolDeployment, vault: Vault): Promise { + const { + tokens, + rateProviders, + priceRateCacheDurations, + swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + amplificationParameter, + from, + } = params; + + const owner = TypesConverter.toAddress(params.owner); + + const pool = await deploy('v2-pool-stable-phantom/StablePhantomPool', { + args: [ + { + vault: vault.address, + name: NAME, + symbol: SYMBOL, + tokens: tokens.addresses, + rateProviders: TypesConverter.toAddresses(rateProviders), + priceRateCacheDurations, + amplificationParameter, + swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + owner, + }, + ], + 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 new file mode 100644 index 0000000000..94cee7a4c5 --- /dev/null +++ b/pvt/helpers/src/models/pools/stable-phantom/types.ts @@ -0,0 +1,46 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; + +import { BigNumberish } from '../../../numbers'; + +import Vault from '../../vault/Vault'; +import TokenList from '../../tokens/TokenList'; +import { Account } from '../../types/types'; + +export type RawStablePhantomPoolDeployment = { + tokens?: TokenList; + swapFeePercentage?: BigNumberish; + amplificationParameter?: BigNumberish; + rateProviders?: Account[]; + priceRateCacheDurations?: BigNumberish[]; + pauseWindowDuration?: BigNumberish; + bufferPeriodDuration?: BigNumberish; + owner?: SignerWithAddress; + admin?: SignerWithAddress; + from?: SignerWithAddress; + vault?: Vault; + mockedVault?: boolean; +}; + +export type StablePhantomPoolDeployment = { + tokens: TokenList; + swapFeePercentage: BigNumberish; + amplificationParameter: BigNumberish; + rateProviders: Account[]; + priceRateCacheDurations: BigNumberish[]; + pauseWindowDuration?: BigNumberish; + bufferPeriodDuration?: BigNumberish; + owner?: SignerWithAddress; + admin?: SignerWithAddress; + from?: SignerWithAddress; +}; + +export type SwapPhantomPool = { + in: number; + out: number; + amount: BigNumberish; + balances: BigNumberish[]; + recipient?: Account; + from?: SignerWithAddress; + lastChangeBlock?: BigNumberish; + data?: string; +}; diff --git a/pvt/helpers/src/models/tokens/TokenList.ts b/pvt/helpers/src/models/tokens/TokenList.ts index bd1bb34aba..63e5e67bd9 100644 --- a/pvt/helpers/src/models/tokens/TokenList.ts +++ b/pvt/helpers/src/models/tokens/TokenList.ts @@ -1,6 +1,11 @@ +import { BigNumber } from 'ethers'; + import Token from './Token'; import TokensDeployer from './TokensDeployer'; import TypesConverter from '../types/TypesConverter'; + +import { Account } from '../types/types'; +import { ZERO_ADDRESS } from '../../constants'; import { RawTokenApproval, RawTokenMint, @@ -9,7 +14,6 @@ import { TokenMint, TokensDeploymentOptions, } from './types'; -import { ZERO_ADDRESS } from '../../constants'; export const ETH_TOKEN_ADDRESS = ZERO_ADDRESS; @@ -100,6 +104,10 @@ export default class TokenList { ); } + async balanceOf(account: Account): Promise { + return Promise.all(this.tokens.map((token) => token.balanceOf(account))); + } + each(fn: (value: Token, i: number, array: Token[]) => void, thisArg?: unknown): void { this.tokens.forEach(fn, thisArg); } diff --git a/pvt/helpers/src/models/types/TypesConverter.ts b/pvt/helpers/src/models/types/TypesConverter.ts index ae41773df7..a0aade3be2 100644 --- a/pvt/helpers/src/models/types/TypesConverter.ts +++ b/pvt/helpers/src/models/types/TypesConverter.ts @@ -1,15 +1,17 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; +import { toNormalizedWeights } from '@balancer-labs/balancer-js'; import { bn, fp } from '../../numbers'; import { DAY, MONTH } from '../../time'; -import { toNormalizedWeights } from '@balancer-labs/balancer-js'; +import { ZERO_ADDRESS } from '../../constants'; import TokenList from '../tokens/TokenList'; import { Account } from './types'; import { RawVaultDeployment, VaultDeployment } from '../vault/types'; -import { RawWeightedPoolDeployment, WeightedPoolDeployment, WeightedPoolType } from '../pools/weighted/types'; import { RawStablePoolDeployment, StablePoolDeployment } from '../pools/stable/types'; import { RawLinearPoolDeployment, LinearPoolDeployment } from '../pools/linear/types'; +import { RawStablePhantomPoolDeployment, StablePhantomPoolDeployment } from '../pools/stable-phantom/types'; +import { RawWeightedPoolDeployment, WeightedPoolDeployment, WeightedPoolType } from '../pools/weighted/types'; import { RawTokenApproval, RawTokenMint, @@ -19,7 +21,6 @@ import { TokenDeployment, RawTokenDeployment, } from '../tokens/types'; -import { ZERO_ADDRESS } from '../../constants'; export function computeDecimalsFromIndex(i: number): number { // Produces repeating series (18..0) @@ -155,6 +156,37 @@ export default { }; }, + toStablePhantomPoolDeployment(params: RawStablePhantomPoolDeployment): StablePhantomPoolDeployment { + let { + tokens, + rateProviders, + priceRateCacheDurations, + amplificationParameter, + swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + } = params; + + if (!tokens) tokens = new TokenList(); + if (!rateProviders) rateProviders = Array(tokens.length).fill(ZERO_ADDRESS); + if (!priceRateCacheDurations) priceRateCacheDurations = Array(tokens.length).fill(DAY); + if (!amplificationParameter) amplificationParameter = bn(200); + if (!swapFeePercentage) swapFeePercentage = bn(1e12); + if (!pauseWindowDuration) pauseWindowDuration = 3 * MONTH; + if (!bufferPeriodDuration) bufferPeriodDuration = MONTH; + + return { + tokens, + rateProviders, + priceRateCacheDurations, + amplificationParameter, + swapFeePercentage, + pauseWindowDuration, + bufferPeriodDuration, + owner: params.owner, + }; + }, + /*** * Converts a raw list of token deployments into a consistent deployment request * @param params It can be a number specifying the number of tokens to be deployed, a list of strings denoting the @@ -223,6 +255,10 @@ export default { ); }, + toAddresses(to: Account[]): string[] { + return to.map(this.toAddress); + }, + toAddress(to?: Account): string { if (!to) return ZERO_ADDRESS; return typeof to === 'string' ? to : to.address; diff --git a/pvt/helpers/src/models/types/types.ts b/pvt/helpers/src/models/types/types.ts index e3ddffbf28..43fb3e38ed 100644 --- a/pvt/helpers/src/models/types/types.ts +++ b/pvt/helpers/src/models/types/types.ts @@ -3,7 +3,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-wit export type NAry = T | Array; -export type Account = string | SignerWithAddress | Contract; +export type Account = string | SignerWithAddress | Contract | { address: string }; export type TxParams = { from?: SignerWithAddress; diff --git a/pvt/helpers/src/models/vault/Vault.ts b/pvt/helpers/src/models/vault/Vault.ts index 59568c32ed..83af9f2351 100644 --- a/pvt/helpers/src/models/vault/Vault.ts +++ b/pvt/helpers/src/models/vault/Vault.ts @@ -100,40 +100,50 @@ export default class Vault { const vault = params.from ? this.instance.connect(params.from) : this.instance; return this.mocked ? vault.callJoinPool( - params.poolAddress, + params.poolAddress ?? ZERO_ADDRESS, params.poolId, - params.recipient, - params.currentBalances, - params.lastChangeBlock, - params.protocolFeePercentage, - params.data + params.recipient ?? ZERO_ADDRESS, + params.currentBalances ?? Array(params.tokens.length).fill(0), + params.lastChangeBlock ?? 0, + params.protocolFeePercentage ?? 0, + params.data ?? '0x' ) - : vault.joinPool(params.poolId, (params.from || (await this._defaultSender())).address, params.recipient, { - assets: params.tokens, - maxAmountsIn: params.maxAmountsIn ?? Array(params.tokens.length).fill(MAX_UINT256), - fromInternalBalance: params.fromInternalBalance ?? false, - userData: params.data, - }); + : vault.joinPool( + params.poolId, + (params.from || (await this._defaultSender())).address, + params.recipient ?? ZERO_ADDRESS, + { + assets: params.tokens, + maxAmountsIn: params.maxAmountsIn ?? Array(params.tokens.length).fill(MAX_UINT256), + fromInternalBalance: params.fromInternalBalance ?? false, + userData: params.data ?? '0x', + } + ); } async exitPool(params: ExitPool): Promise { const vault = params.from ? this.instance.connect(params.from) : this.instance; return this.mocked ? vault.callExitPool( - params.poolAddress, + params.poolAddress ?? ZERO_ADDRESS, params.poolId, - params.recipient, - params.currentBalances, - params.lastChangeBlock, - params.protocolFeePercentage, - params.data + params.recipient ?? ZERO_ADDRESS, + params.currentBalances ?? Array(params.tokens.length).fill(0), + params.lastChangeBlock ?? 0, + params.protocolFeePercentage ?? 0, + params.data ?? '0x' ) - : vault.exitPool(params.poolId, (params.from || (await this._defaultSender())).address, params.recipient, { - assets: params.tokens, - minAmountsOut: params.minAmountsOut ?? Array(params.tokens.length).fill(0), - toInternalBalance: params.toInternalBalance ?? false, - userData: params.data, - }); + : vault.exitPool( + params.poolId, + (params.from || (await this._defaultSender())).address, + params.recipient ?? ZERO_ADDRESS, + { + assets: params.tokens, + minAmountsOut: params.minAmountsOut ?? Array(params.tokens.length).fill(0), + toInternalBalance: params.toInternalBalance ?? false, + userData: params.data ?? '0x', + } + ); } async getCollectedFeeAmounts(tokens: TokenList | string[]): Promise { diff --git a/pvt/helpers/src/models/vault/types.ts b/pvt/helpers/src/models/vault/types.ts index 6f89546eae..81662f7253 100644 --- a/pvt/helpers/src/models/vault/types.ts +++ b/pvt/helpers/src/models/vault/types.ts @@ -44,28 +44,28 @@ export type GeneralSwap = Swap & { }; export type JoinPool = { - poolAddress: string; poolId: string; - recipient: string; - currentBalances: BigNumberish[]; tokens: string[]; - lastChangeBlock: BigNumberish; - protocolFeePercentage: BigNumberish; - data: string; + poolAddress?: string; + recipient?: string; + currentBalances?: BigNumberish[]; + lastChangeBlock?: BigNumberish; + protocolFeePercentage?: BigNumberish; + data?: string; maxAmountsIn?: BigNumberish[]; fromInternalBalance?: boolean; from?: SignerWithAddress; }; export type ExitPool = { - poolAddress: string; poolId: string; - recipient: string; - currentBalances: BigNumberish[]; tokens: string[]; - lastChangeBlock: BigNumberish; - protocolFeePercentage: BigNumberish; - data: string; + poolAddress?: string; + recipient?: string; + currentBalances?: BigNumberish[]; + lastChangeBlock?: BigNumberish; + protocolFeePercentage?: BigNumberish; + data?: string; minAmountsOut?: BigNumberish[]; toInternalBalance?: boolean; from?: SignerWithAddress;