diff --git a/package.json b/package.json index aebf7afb..33c578e4 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,10 @@ "release": "release-it --non-interactive", "changelog": "auto-changelog -p", "prepublishOnly": "npm run build", - "test:pool": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/Router.test.ts'", + "test:pool": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/Pool.test.ts'", "test:dt": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/Datatoken.test.ts'", "test:nftDt": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/NFTDatatoken.test.ts'", + "test:router": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/Router.test.ts'", "test:unit": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/**/*.test.ts'", "test:unit:cover": "nyc --report-dir coverage/unit npm run test:unit", "test:integration": "mocha --config=test/integration/.mocharc.json --node-env=test --exit 'test/integration/**/*.test.ts'", diff --git a/src/factories/NFTFactory.ts b/src/factories/NFTFactory.ts index 809fe58b..ce236cb7 100644 --- a/src/factories/NFTFactory.ts +++ b/src/factories/NFTFactory.ts @@ -1,5 +1,6 @@ import { Contract } from 'web3-eth-contract' import Web3 from 'web3' +import BigNumber from 'bignumber.js' import { TransactionReceipt } from 'web3-core' import { AbiItem } from 'web3-utils' import defaultFactory721ABI from '@oceanprotocol/contracts/artifacts/contracts/ERC721Factory.sol/ERC721Factory.json' @@ -37,7 +38,7 @@ interface ErcCreateData { interface PoolData { addresses: string[] - ssParams: (string | number)[] + ssParams: (string | number | BigNumber)[] swapFees: number[] } diff --git a/src/pools/Router.ts b/src/pools/Router.ts index 7187a142..e1606b32 100644 --- a/src/pools/Router.ts +++ b/src/pools/Router.ts @@ -130,16 +130,23 @@ export class Router { } /** - * Estimate gas cost for addOceanToken method + * Estimate gas cost for addOceanToken * @param {String} address - * @param {String} tokenAddress template address to add - * @return {Promise} + * @param {String} tokenAddress token address we want to add + * @param {Contract} routerContract optional contract instance + * @return {Promise} */ - public async estGasAddOceanToken(address: string, tokenAddress: string): Promise { + public async estGasAddOceanToken( + address: string, + tokenAddress: string, + contractInstance?: Contract + ) { + const routerContract = contractInstance || this.router + const gasLimitDefault = this.GASLIMIT_DEFAULT let estGas try { - estGas = await this.router.methods + estGas = await routerContract.methods .addOceanToken(tokenAddress) .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) } catch (e) { @@ -150,8 +157,8 @@ export class Router { /** * Add a new token to oceanTokens list, pools with basetoken in this list have NO opf Fee - * @param {String} address - * @param {String} tokenAddress template address to add + * @param {String} address caller address + * @param {String} tokenAddress token address to add * @return {Promise} */ public async addOceanToken( @@ -175,25 +182,28 @@ export class Router { } /** - * Estimate gas cost for removeOceanToken method - * @param {String} address - * @param {String} tokenAddress address to remove - * @return {Promise} + * Estimate gas cost for removeOceanToken + * @param {String} address caller address + * @param {String} tokenAddress token address we want to add + * @param {Contract} routerContract optional contract instance + * @return {Promise} */ public async estGasRemoveOceanToken( address: string, - tokenAddress: string - ): Promise { + tokenAddress: string, + contractInstance?: Contract + ) { + const routerContract = contractInstance || this.router + const gasLimitDefault = this.GASLIMIT_DEFAULT let estGas try { - estGas = await this.router.methods + estGas = await routerContract.methods .removeOceanToken(tokenAddress) .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) } catch (e) { estGas = gasLimitDefault } - return estGas } diff --git a/src/pools/balancer/Pool.ts b/src/pools/balancer/Pool.ts new file mode 100644 index 00000000..d6a5585f --- /dev/null +++ b/src/pools/balancer/Pool.ts @@ -0,0 +1,1636 @@ +import Web3 from 'web3' +import { AbiItem } from 'web3-utils/types' +import { TransactionReceipt } from 'web3-core' +import { Contract } from 'web3-eth-contract' +import { Logger, getFairGasPrice } from '../../utils' +import BigNumber from 'bignumber.js' +import PoolTemplate from '@oceanprotocol/contracts/artifacts/contracts/pools/balancer/BPool.sol/BPool.json' +import defaultPool from '@oceanprotocol/contracts/artifacts/contracts/pools/FactoryRouter.sol/FactoryRouter.json' +import defaultERC20ABI from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' +import Decimal from 'decimal.js' + +const MaxUint256 = + '115792089237316195423570985008687907853269984665640564039457584007913129639934' +/** + * Provides an interface to Ocean friendly fork from Balancer BPool + */ +// TODO: Add decimals handling +export class Pool { + public poolABI: AbiItem | AbiItem[] + public web3: Web3 + public GASLIMIT_DEFAULT = 1000000 + private logger: Logger + + constructor(web3: Web3, logger: Logger, poolABI: AbiItem | AbiItem[] = null) { + if (poolABI) this.poolABI = poolABI + else this.poolABI = PoolTemplate.abi as AbiItem[] + this.web3 = web3 + this.logger = logger + } + + /** + * Estimate gas cost for collectMarketFee + * @param {String} account + * @param {String} tokenAddress + * @param {String} spender + * @param {String} amount + * @param {String} force + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estApprove( + account: string, + tokenAddress: string, + spender: string, + amount: string, + contractInstance?: Contract + ): Promise { + const tokenContract = + contractInstance || + new this.web3.eth.Contract(defaultERC20ABI.abi as AbiItem[], tokenAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await tokenContract.methods + .approve(spender, amount) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Get Alloance for both DataToken and Ocean + * @param {String } tokenAdress + * @param {String} owner + * @param {String} spender + */ + public async allowance( + tokenAddress: string, + owner: string, + spender: string + ): Promise { + const tokenAbi = defaultERC20ABI.abi as AbiItem[] + const datatoken = new this.web3.eth.Contract(tokenAbi, tokenAddress) + const trxReceipt = await datatoken.methods.allowance(owner, spender).call() + + return await this.unitsToAmount(tokenAddress, trxReceipt) + } + + /** + * Approve spender to spent amount tokens + * @param {String} account + * @param {String} tokenAddress + * @param {String} spender + * @param {String} amount (always expressed as wei) + * @param {String} force if true, will overwrite any previous allowence. Else, will check if allowence is enough and will not send a transaction if it's not needed + */ + async approve( + account: string, + tokenAddress: string, + spender: string, + amount: string, + force = false + ): Promise { + const minABI = [ + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'approve', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + } + ] as AbiItem[] + const token = new this.web3.eth.Contract(minABI, tokenAddress) + if (!force) { + const currentAllowence = await this.allowance(tokenAddress, account, spender) + if (new Decimal(currentAllowence).greaterThanOrEqualTo(amount)) { + return currentAllowence + } + } + let result = null + const amountFormatted = await this.amountToUnits(tokenAddress, amount) + const estGas = await this.estApprove(account, tokenAddress, spender, amountFormatted) + + try { + result = await token.methods + .approve(spender, new BigNumber(await this.amountToUnits(tokenAddress, amount))) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERRPR: Failed to approve spender to spend tokens : ${e.message}`) + } + return result + } + + /** + * Get user shares of pool tokens + * @param {String} account + * @param {String} poolAddress + * @return {String} + */ + async sharesBalance(account: string, poolAddress: string): Promise { + let result = null + try { + const token = new this.web3.eth.Contract(this.poolABI, poolAddress) + const balance = await token.methods.balanceOf(account).call() + result = this.web3.utils.fromWei(balance) + } catch (e) { + this.logger.error(`ERROR: Failed to get shares of pool : ${e.message}`) + } + return result + } + + /** + * Set pool fee + * @param {String} account + * @param {String} poolAddress + * @param {String} fee 0.1=10% fee(max allowed) + */ + async setSwapFee( + account: string, + poolAddress: string, + fee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress, { + from: account + }) + let result = null + try { + result = await pool.methods.setSwapFee(this.web3.utils.toWei(fee)).send({ + from: account, + gas: this.GASLIMIT_DEFAULT, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to set pool swap fee: ${e.message}`) + } + return result + } + + /** + * Get number of tokens composing this pool + * @param {String} poolAddress + * @return {String} + */ + async getNumTokens(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getNumTokens().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get number of tokens: ${e.message}`) + } + return result + } + + /** + * Get total supply of pool shares + * @param {String} poolAddress + * @return {String} + */ + async getPoolSharesTotalSupply(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods.totalSupply().call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to get total supply of pool shares: ${e.message}`) + } + return amount + } + + /** + * Get tokens composing this pool + * @param {String} poolAddress + * @return {String[]} + */ + async getCurrentTokens(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getCurrentTokens().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get tokens composing this pool: ${e.message}`) + } + return result + } + + /** + * Get the final tokens composing this pool + * @param {String} poolAddress + * @return {String[]} + */ + async getFinalTokens(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getFinalTokens().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get the final tokens composing this pool`) + } + return result + } + + /** + * Get controller address of this pool + * @param {String} poolAddress + * @return {String} + */ + async getController(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getController().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get pool controller address: ${e.message}`) + } + return result + } + + /** + * Get basetoken address of this pool + * @param {String} poolAddress + * @return {String} + */ + async getBasetoken(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getBaseTokenAddress().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get basetoken address: ${e.message}`) + } + return result + } + + /** + * Get datatoken address of this pool + * @param {String} poolAddress + * @return {String} + */ + async getDatatoken(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.getDataTokenAddress().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get datatoken address: ${e.message}`) + } + return result + } + + /** + * Get marketFeeCollector of this pool + * @param {String} poolAddress + * @return {String} + */ + async getMarketFeeCollector(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods._marketCollector().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get marketFeeCollector address: ${e.message}`) + } + return result + } + + /** + * Get OPF Collector of this pool + * @param {String} poolAddress + * @return {String} + */ + async getOPFCollector(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods._opfCollector().call() + } catch (e) { + this.logger.error(`ERROR: Failed to get OPF Collector address: ${e.message}`) + } + return result + } + + /** + * Get if a token is bounded to a pool + * @param {String} poolAddress + * @param {String} token Address of the token + * @return {Boolean} + */ + async isBound(poolAddress: string, token: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.isBound(token).call() + } catch (e) { + this.logger.error(`ERROR: Failed to check whether a token \ + bounded to a pool. ${e.message}`) + } + return result + } + + /** + * Get how many tokens are in the pool + * @param {String} poolAddress + * @param {String} token Address of the token + * @return {String} + */ + async getReserve(poolAddress: string, token: string): Promise { + let amount = null + try { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + const result = await pool.methods.getBalance(token).call() + amount = await this.unitsToAmount(token, result) + } catch (e) { + this.logger.error(`ERROR: Failed to get how many tokens \ + are in the pool: ${e.message}`) + } + return amount.toString() + } + + /** + * Get if a pool is finalized + * @param {String} poolAddress + * @return {Boolean} + */ + async isFinalized(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + try { + result = await pool.methods.isFinalized().call() + } catch (e) { + this.logger.error(`ERROR: Failed to check whether pool is finalized: ${e.message}`) + } + return result + } + + /** + * Get pool fee + * @param {String} poolAddress + * @return {String} Swap fee. To get the percentage value, substract by 100. E.g. `0.1` represents a 10% swap fee. + */ + async getSwapFee(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let fee = null + try { + const result = await pool.methods.getSwapFee().call() + fee = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to get pool fee: ${e.message}`) + } + return fee + } + + /** + * The normalized weight of a token. The combined normalized weights of all tokens will sum up to 1. (Note: the actual sum may be 1 plus or minus a few wei due to division precision loss) + * @param {String} poolAddress + * @param {String} token + * @return {String} + */ + async getNormalizedWeight(poolAddress: string, token: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let weight = null + try { + const result = await pool.methods.getNormalizedWeight(token).call() + weight = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to get normalized weight of a token: ${e.message}`) + } + return weight + } + + /** + * getDenormalizedWeight of a token in pool + * @param {String} poolAddress + * @param {String} token + * @return {String} + */ + async getDenormalizedWeight(poolAddress: string, token: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let weight = null + try { + const result = await pool.methods.getDenormalizedWeight(token).call() + weight = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error('ERROR: Failed to get denormalized weight of a token in pool') + } + return weight + } + + /** + * getTotalDenormalizedWeight in pool + * @param {String} poolAddress + * @return {String} + */ + async getTotalDenormalizedWeight(poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let weight = null + try { + const result = await pool.methods.getTotalDenormalizedWeight().call() + weight = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error('ERROR: Failed to get total denormalized weight in pool') + } + return weight + } + + /** + * Get Market Fees available to be collected for a specific token + * @param {String} poolAddress + * @param {String} token token we want to check fees + * @return {String} + */ + async getMarketFees(poolAddress: string, token: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let weight = null + try { + const result = await pool.methods.marketFees(token).call() + weight = await this.unitsToAmount(token, result) + } catch (e) { + this.logger.error(`ERROR: Failed to get market fees for a token: ${e.message}`) + } + return weight + } + + /** + * Get Community Fees available to be collected for a specific token + * @param {String} poolAddress + * @param {String} token token we want to check fees + * @return {String} + */ + async getCommunityFees(poolAddress: string, token: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let weight = null + try { + const result = await pool.methods.communityFees(token).call() + weight = await this.unitsToAmount(token, result) + } catch (e) { + this.logger.error(`ERROR: Failed to get community fees for a token: ${e.message}`) + } + return weight + } + + /** + * Estimate gas cost for collectOPF + * @param {String} address + * @param {String} poolAddress + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estCollectOPF( + address: string, + poolAddress: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .collectOPF() + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * collectOPF - collect opf fee - can be called by anyone + * @param {String} address + * @param {String} poolAddress + * @return {TransactionReceipt} + */ + async collectOPF(address: string, poolAddress: string): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + const estGas = await this.estCollectOPF(address, poolAddress) + + try { + result = await pool.methods.collectOPF().send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to swap exact amount in : ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for collectMarketFee + * @param {String} address + * @param {String} poolAddress + * @param {String} to address that will receive fees + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estCollectMarketFee( + address: string, + poolAddress: string, + to: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .collectMarketFee(to) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * collectOPF - collect market fees - can be called by the marketFeeCollector + * @param {String} address + * @param {String} poolAddress + * @param {String} to address that will receive fees + * @return {TransactionReceipt} + */ + async collectMarketFee( + address: string, + poolAddress: string, + to: string + ): Promise { + if ((await this.getMarketFeeCollector(poolAddress)) !== address) { + throw new Error(`Caller is not MarketFeeCollector`) + } + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + const estGas = await this.estCollectMarketFee(address, poolAddress, to) + + try { + result = await pool.methods.collectMarketFee(to).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to swap exact amount in : ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for collectMarketFee + * @param {String} address + * @param {String} poolAddress + * @param {String} newCollector new market fee collector address + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estUpdateMarketFeeCollector( + address: string, + poolAddress: string, + newCollector: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .updateMarketFeeCollector(newCollector) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * updateMarketFeeCollector - updates marketFeeCollector - can be called only by the marketFeeCollector + * @param {String} address + * @param {String} poolAddress + * @param {String} newCollector new market fee collector address + * @return {TransactionReceipt} + */ + async updateMarketFeeCollector( + address: string, + poolAddress: string, + newCollector: string + ): Promise { + if ((await this.getMarketFeeCollector(poolAddress)) !== address) { + throw new Error(`Caller is not MarketFeeCollector`) + } + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + const estGas = await this.estUpdateMarketFeeCollector( + address, + poolAddress, + newCollector + ) + + try { + result = await pool.methods.updateMarketFeeCollector(newCollector).send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to swap exact amount in : ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for swapExactAmountIn + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} tokenOut + * @param {String} minAmountOut will be converted to wei + * @param {String} maxPrice will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estSwapExactAmountIn( + address: string, + poolAddress: string, + tokenIn: string, + tokenAmountIn: string, + tokenOut: string, + minAmountOut: string, + maxPrice: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .swapExactAmountIn( + tokenIn, + tokenAmountIn, + tokenOut, + minAmountOut, + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + async amountToUnits(token: string, amount: string): Promise { + let decimals = 18 + const tokenContract = new this.web3.eth.Contract( + defaultERC20ABI.abi as AbiItem[], + token + ) + try { + decimals = await tokenContract.methods.decimals().call() + } catch (e) { + this.logger.error('ERROR: FAILED TO CALL DECIMALS(), USING 18') + } + + const amountFormatted = new BigNumber(parseInt(amount) * 10 ** decimals) + + return amountFormatted.toString() + } + + async unitsToAmount(token: string, amount: string): Promise { + let decimals = 18 + const tokenContract = new this.web3.eth.Contract( + defaultERC20ABI.abi as AbiItem[], + token + ) + try { + decimals = await tokenContract.methods.decimals().call() + } catch (e) { + this.logger.error('ERROR: FAILED TO CALL DECIMALS(), USING 18') + } + + const amountFormatted = new BigNumber(parseInt(amount) / 10 ** decimals) + + return amountFormatted.toString() + } + + /** + * swapExactAmountIn - Trades an exact tokenAmountIn of tokenIn taken from the caller by the pool, in exchange for at least minAmountOut of tokenOut given to the caller from the pool, with a maximum marginal price of maxPrice. Returns (tokenAmountOut, spotPriceAfter), where tokenAmountOut is the amount of token that came out of the pool, and spotPriceAfter is the new marginal spot price, ie, the result of getSpotPrice after the call. (These values are what are limited by the arguments; you are guaranteed tokenAmountOut >= minAmountOut and spotPriceAfter <= maxPrice). + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} tokenOut + * @param {String} minAmountOut will be converted to wei + * @param {String} maxPrice will be converted to wei + * @return {TransactionReceipt} + */ + async swapExactAmountIn( + address: string, + poolAddress: string, + tokenIn: string, + tokenAmountIn: string, + tokenOut: string, + minAmountOut: string, + maxPrice?: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + + const amountInFormatted = await this.amountToUnits(tokenIn, tokenAmountIn) + + const minAmountOutFormatted = await this.amountToUnits(tokenOut, minAmountOut) + + let result = null + + const estGas = await this.estSwapExactAmountIn( + address, + poolAddress, + tokenIn, + amountInFormatted, + tokenOut, + minAmountOutFormatted.toString(), + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + console.log(minAmountOutFormatted, 'minamoutnoutformatted') + try { + result = await pool.methods + .swapExactAmountIn( + tokenIn, + amountInFormatted, + tokenOut, + minAmountOutFormatted, + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to swap exact amount in : ${e.message}`) + } + + return result + } + + /** + * Estimate gas cost for swapExactAmountOut + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} tokenOut + * @param {String} minAmountOut will be converted to wei + * @param {String} maxPrice will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estSwapExactAmountOut( + address: string, + poolAddress: string, + tokenIn: string, + maxAmountIn: string, + tokenOut: string, + amountOut: string, + maxPrice?: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .swapExactAmountOut( + tokenIn, + maxAmountIn, + tokenOut, + amountOut, + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * swapExactAmountOut + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} maxAmountIn will be converted to wei + * @param {String} tokenOut + * @param {String} amountOut will be converted to wei + * @param {String} maxPrice will be converted to wei + * @return {TransactionReceipt} + */ + async swapExactAmountOut( + account: string, + poolAddress: string, + tokenIn: string, + maxAmountIn: string, + tokenOut: string, + amountOut: string, + maxPrice?: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + + const maxAmountInFormatted = await this.amountToUnits(tokenIn, maxAmountIn) + const amountOutFormatted = await this.amountToUnits(tokenOut, amountOut) + const estGas = await this.estSwapExactAmountOut( + account, + poolAddress, + tokenIn, + maxAmountInFormatted, + tokenOut, + amountOutFormatted, + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + + try { + result = await pool.methods + .swapExactAmountOut( + tokenIn, + maxAmountInFormatted, + tokenOut, + amountOutFormatted, + maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to swap exact amount out: ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for swapExactAmountOut + * @param {String} address + * @param {String} poolAddress + * @param {String} poolAmountOut will be converted to wei + * @param {String[]} maxAmountsIn array holding maxAmount per each token, will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estJoinPool( + address: string, + poolAddress: string, + poolAmountOut: string, + maxAmountsIn: string[], + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .joinPool(this.web3.utils.toWei(poolAmountOut), maxAmountsIn) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Join the pool, getting poolAmountOut pool tokens. This will pull some of each of the currently trading tokens in the pool, meaning you must have called approve for each token for this pool. These values are limited by the array of maxAmountsIn in the order of the pool tokens. + * @param {String} address + * @param {String} poolAddress + * @param {String} poolAmountOut will be converted to wei + * @param {String[]} maxAmountsIn array holding maxAmount per each token, will be converted to wei + * @return {TransactionReceipt} + */ + async joinPool( + address: string, + poolAddress: string, + poolAmountOut: string, + maxAmountsIn: string[] + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + const weiMaxAmountsIn = [] + const tokens = await this.getFinalTokens(poolAddress) + + for (let i = 0; i < 2; i++) { + const amount = await this.amountToUnits(tokens[i], maxAmountsIn[i]) + weiMaxAmountsIn.push(amount) + } + // console.log(weiMaxAmountsIn) + + let result = null + + const estGas = await this.estJoinPool( + address, + poolAddress, + this.web3.utils.toWei(poolAmountOut), + weiMaxAmountsIn + ) + + try { + result = await pool.methods + .joinPool(this.web3.utils.toWei(poolAmountOut), weiMaxAmountsIn) + .send({ + from: address, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to join pool: ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for exitPool +* @param {String} address + * @param {String} poolAddress + ``* @param {String} poolAmountIn will be converted to wei + * @param {String[]} minAmountsOut array holding minAmount per each token, will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estExitPool( + address: string, + poolAddress: string, + poolAmountIn: string, + minAmountsOut: string[], + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .exitPool(this.web3.utils.toWei(poolAmountIn), minAmountsOut) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Exit the pool, paying poolAmountIn pool tokens and getting some of each of the currently trading tokens in return. These values are limited by the array of minAmountsOut in the order of the pool tokens. + * @param {String} account + * @param {String} poolAddress + * @param {String} poolAmountIn will be converted to wei + * @param {String[]} minAmountsOut array holding minAmount per each token, will be converted to wei + * @return {TransactionReceipt} + */ + async exitPool( + account: string, + poolAddress: string, + poolAmountIn: string, + minAmountsOut: string[] + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + const weiMinAmountsOut = [] + const tokens = await this.getFinalTokens(poolAddress) + + for (let i = 0; i < 2; i++) { + const amount = await this.amountToUnits(tokens[i], minAmountsOut[i]) + weiMinAmountsOut.push(amount) + } + let result = null + const estGas = await this.estExitPool( + account, + poolAddress, + this.web3.utils.toWei(poolAmountIn), + weiMinAmountsOut + ) + try { + result = await pool.methods + .exitPool(this.web3.utils.toWei(poolAmountIn), weiMinAmountsOut) + .send({ from: account, gas: estGas, gasPrice: await getFairGasPrice(this.web3) }) + } catch (e) { + this.logger.error(`ERROR: Failed to exit pool: ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for joinswapExternAmountIn + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} minPoolAmountOut will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estJoinswapExternAmountIn( + address: string, + poolAddress: string, + tokenIn: string, + tokenAmountIn: string, + minPoolAmountOut: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .joinswapExternAmountIn(tokenIn, tokenAmountIn, minPoolAmountOut) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Pay tokenAmountIn of token tokenIn to join the pool, getting poolAmountOut of the pool shares. + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenAmountIn will be converted to wei + * @param {String} minPoolAmountOut will be converted to wei + * @return {TransactionReceipt} + */ + async joinswapExternAmountIn( + account: string, + poolAddress: string, + tokenIn: string, + tokenAmountIn: string, + minPoolAmountOut: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + + const amountInFormatted = await this.amountToUnits(tokenIn, tokenAmountIn) + const estGas = await this.estJoinswapExternAmountIn( + account, + poolAddress, + tokenIn, + amountInFormatted, + this.web3.utils.toWei(minPoolAmountOut) + ) + + try { + result = await pool.methods + .joinswapExternAmountIn( + tokenIn, + amountInFormatted, + this.web3.utils.toWei(minPoolAmountOut) + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to pay tokens in order to \ + join the pool: ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for joinswapPoolAmountOut + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} poolAmountOut will be converted to wei + * @param {String} maxAmountIn will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estJoinswapPoolAmountOut( + address: string, + poolAddress: string, + tokenIn: string, + poolAmountOut: string, + maxAmountIn: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .joinswapPoolAmountOut(tokenIn, poolAmountOut, maxAmountIn) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Specify poolAmountOut pool shares that you want to get, and a token tokenIn to pay with. This costs tokenAmountIn tokens (these went into the pool). + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} poolAmountOut will be converted to wei + * @param {String} maxAmountIn will be converted to wei + * @return {TransactionReceipt} + */ + async joinswapPoolAmountOut( + account: string, + poolAddress: string, + tokenIn: string, + poolAmountOut: string, + maxAmountIn: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + + const maxAmountInFormatted = await this.amountToUnits(tokenIn, maxAmountIn) + const estGas = await this.estJoinswapPoolAmountOut( + account, + poolAddress, + tokenIn, + this.web3.utils.toWei(poolAmountOut), + maxAmountInFormatted + ) + try { + result = await pool.methods + .joinswapPoolAmountOut( + tokenIn, + this.web3.utils.toWei(poolAmountOut), + maxAmountInFormatted + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error('ERROR: Failed to join swap pool amount out') + } + return result + } + + /** + * Estimate gas cost for joinswapExternAmountIn + * @param {String} address + @param {String} poolAddress + * @param {String} tokenOut + * @param {String} poolAmountIn will be converted to wei + * @param {String} minTokenAmountOut will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estExitswapPoolAmountIn( + address: string, + poolAddress: string, + tokenOut: string, + poolAmountIn: string, + minTokenAmountOut: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .exitswapPoolAmountIn(tokenOut, poolAmountIn, minTokenAmountOut) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Pay poolAmountIn pool shares into the pool, getting minTokenAmountOut of the given token tokenOut out of the pool. + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenOut + * @param {String} poolAmountIn will be converted to wei + * @param {String} minTokenAmountOut will be converted to wei + * @return {TransactionReceipt} + */ + async exitswapPoolAmountIn( + account: string, + poolAddress: string, + tokenOut: string, + poolAmountIn: string, + minTokenAmountOut: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + + const minTokenOutFormatted = await this.amountToUnits(tokenOut, minTokenAmountOut) + const estGas = await this.estExitswapPoolAmountIn( + account, + poolAddress, + tokenOut, + this.web3.utils.toWei(poolAmountIn), + minTokenOutFormatted + ) + try { + result = await pool.methods + .exitswapPoolAmountIn( + tokenOut, + this.web3.utils.toWei(poolAmountIn), + minTokenOutFormatted + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error(`ERROR: Failed to pay pool shares into the pool: ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for joinswapExternAmountIn + * @param {String} address + * @param {String} poolAddress + * @param {String} tokenOut + * @param {String} tokenAmountOut will be converted to wei + * @param {String} maxPoolAmountIn will be converted to wei + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estExitswapExternAmountOut( + address: string, + poolAddress: string, + tokenOut: string, + tokenAmountOut: string, + maxPoolAmountIn: string, + contractInstance?: Contract + ): Promise { + const poolContract = + contractInstance || + new this.web3.eth.Contract(this.poolABI as AbiItem[], poolAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await poolContract.methods + .exitswapExternAmountOut(tokenOut, tokenAmountOut, maxPoolAmountIn) + .estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * Specify tokenAmountOut of token tokenOut that you want to get out of the pool. This costs poolAmountIn pool shares (these went into the pool). + * @param {String} account + * @param {String} poolAddress + * @param {String} tokenOut + * @param {String} tokenAmountOut will be converted to wei + * @param {String} maxPoolAmountIn will be converted to wei + * @return {TransactionReceipt} + */ + async exitswapExternAmountOut( + account: string, + poolAddress: string, + tokenOut: string, + tokenAmountOut: string, + maxPoolAmountIn: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let result = null + + const estGas = await this.estExitswapExternAmountOut( + account, + poolAddress, + tokenOut, + this.web3.utils.toWei(tokenAmountOut), + this.web3.utils.toWei(maxPoolAmountIn) + ) + try { + result = await pool.methods + .exitswapExternAmountOut( + tokenOut, + this.web3.utils.toWei(tokenAmountOut), + this.web3.utils.toWei(maxPoolAmountIn) + ) + .send({ + from: account, + gas: estGas + 1, + gasPrice: await getFairGasPrice(this.web3) + }) + } catch (e) { + this.logger.error('ERROR: Failed to exitswapExternAmountOut') + } + return result + } + + /** + * Get Spot Price of swaping tokenIn to tokenOut + * @param {String} poolAddress + * @param {String} tokenIn + * @param {String} tokenOut + * @return {String} + */ + async getSpotPrice( + poolAddress: string, + tokenIn: string, + tokenOut: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let decimalsTokenIn = 18 + let decimalsTokenOut = 18 + + const tokenInContract = new this.web3.eth.Contract( + defaultERC20ABI.abi as AbiItem[], + tokenIn + ) + const tokenOutContract = new this.web3.eth.Contract( + defaultERC20ABI.abi as AbiItem[], + tokenOut + ) + try { + decimalsTokenIn = await tokenInContract.methods.decimals().call() + } catch (e) { + this.logger.error('ERROR: FAILED TO CALL DECIMALS(), USING 18') + } + try { + decimalsTokenOut = await tokenOutContract.methods.decimals().call() + } catch (e) { + this.logger.error('ERROR: FAILED TO CALL DECIMALS(), USING 18') + } + + let price = null + try { + price = await pool.methods.getSpotPrice(tokenIn, tokenOut).call() + price = new BigNumber(price.toString()) + } catch (e) { + this.logger.error('ERROR: Failed to get spot price of swapping tokenIn to tokenOut') + } + + let decimalsDiff + if (decimalsTokenIn > decimalsTokenOut) { + decimalsDiff = decimalsTokenIn - decimalsTokenOut + price = new BigNumber(price / 10 ** decimalsDiff) + // console.log(price.toString()) + price = price / 10 ** decimalsTokenOut + // console.log('dtIn') + } else { + decimalsDiff = decimalsTokenOut - decimalsTokenIn + price = new BigNumber(price * 10 ** (2 * decimalsDiff)) + price = price / 10 ** decimalsTokenOut + // console.log('usdcIn') + } + + return price.toString() + } + + public async getAmountInExactOut( + poolAddress: string, + tokenIn: string, + tokenOut: string, + tokenAmountOut: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + + const amountOutFormatted = await this.amountToUnits(tokenOut, tokenAmountOut) + + let amount = null + + try { + const result = await pool.methods + .getAmountInExactOut(tokenIn, tokenOut, amountOutFormatted) + .call() + amount = await this.unitsToAmount(tokenIn, result) + } catch (e) { + this.logger.error('ERROR: Failed to calcInGivenOut') + } + return amount + } + + public async getAmountOutExactIn( + poolAddress: string, + tokenIn: string, + tokenOut: string, + tokenAmountIn: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + + const amountInFormatted = await this.amountToUnits(tokenIn, tokenAmountIn) + + let amount = null + + try { + const result = await pool.methods + .getAmountOutExactIn(tokenIn, tokenOut, amountInFormatted) + .call() + amount = await this.unitsToAmount(tokenOut, result) + } catch (e) { + this.logger.error('ERROR: Failed to calcOutGivenIn') + } + return amount + } + + public async calcPoolOutGivenSingleIn( + poolAddress: string, + tokenBalanceIn: string, + tokenWeightIn: string, + poolSupply: string, + totalWeight: string, + tokenAmountIn: string, + swapFee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods + .calcPoolOutGivenSingleIn( + this.web3.utils.toWei(tokenBalanceIn), + this.web3.utils.toWei(tokenWeightIn), + this.web3.utils.toWei(poolSupply), + this.web3.utils.toWei(totalWeight), + this.web3.utils.toWei(tokenAmountIn), + this.web3.utils.toWei(swapFee) + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to calculate PoolOutGivenSingleIn : ${e.message}`) + } + return amount + } + + public async calcSingleInGivenPoolOut( + poolAddress: string, + tokenBalanceIn: string, + tokenWeightIn: string, + poolSupply: string, + totalWeight: string, + poolAmountOut: string, + swapFee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods + .calcSingleInGivenPoolOut( + this.web3.utils.toWei(tokenBalanceIn), + this.web3.utils.toWei(tokenWeightIn), + this.web3.utils.toWei(poolSupply), + this.web3.utils.toWei(totalWeight), + this.web3.utils.toWei(poolAmountOut), + this.web3.utils.toWei(swapFee) + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to calculate SingleInGivenPoolOut : ${e.message}`) + } + return amount + } + + public async calcSingleOutGivenPoolIn( + poolAddress: string, + tokenBalanceOut: string, + tokenWeightOut: string, + poolSupply: string, + totalWeight: string, + poolAmountIn: string, + swapFee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods + .calcSingleOutGivenPoolIn( + this.web3.utils.toWei(tokenBalanceOut), + this.web3.utils.toWei(tokenWeightOut), + this.web3.utils.toWei(poolSupply), + this.web3.utils.toWei(totalWeight), + this.web3.utils.toWei(poolAmountIn), + this.web3.utils.toWei(swapFee) + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to calculate SingleOutGivenPoolIn : ${e.message}`) + } + return amount + } + + public async calcPoolInGivenSingleOut( + poolAddress: string, + tokenBalanceOut: string, + tokenWeightOut: string, + poolSupply: string, + totalWeight: string, + tokenAmountOut: string, + swapFee: string + ): Promise { + const pool = new this.web3.eth.Contract(this.poolABI, poolAddress) + let amount = null + try { + const result = await pool.methods + .calcPoolInGivenSingleOut( + this.web3.utils.toWei(tokenBalanceOut), + this.web3.utils.toWei(tokenWeightOut), + this.web3.utils.toWei(poolSupply), + this.web3.utils.toWei(totalWeight), + this.web3.utils.toWei(tokenAmountOut), + this.web3.utils.toWei(swapFee) + ) + .call() + amount = this.web3.utils.fromWei(result) + } catch (e) { + this.logger.error(`ERROR: Failed to calculate PoolInGivenSingleOut : ${e.message}`) + } + return amount + } + + /** + * Get LOG_SWAP encoded topic + * @return {String} + */ + public getSwapEventSignature(): string { + const abi = this.poolABI as AbiItem[] + const eventdata = abi.find(function (o) { + if (o.name === 'LOG_SWAP' && o.type === 'event') return o + }) + const topic = this.web3.eth.abi.encodeEventSignature(eventdata as any) + return topic + } + + /** + * Get LOG_JOIN encoded topic + * @return {String} + */ + public getJoinEventSignature(): string { + const abi = this.poolABI as AbiItem[] + const eventdata = abi.find(function (o) { + if (o.name === 'LOG_JOIN' && o.type === 'event') return o + }) + const topic = this.web3.eth.abi.encodeEventSignature(eventdata as any) + return topic + } + + /** + * Get LOG_EXIT encoded topic + * @return {String} + */ + public getExitEventSignature(): string { + const abi = this.poolABI as AbiItem[] + const eventdata = abi.find(function (o) { + if (o.name === 'LOG_EXIT' && o.type === 'event') return o + }) + const topic = this.web3.eth.abi.encodeEventSignature(eventdata as any) + return topic + } +} diff --git a/src/pools/balancer/index.ts b/src/pools/balancer/index.ts index 9c700234..0d56f727 100644 --- a/src/pools/balancer/index.ts +++ b/src/pools/balancer/index.ts @@ -1,2 +1 @@ -export * from './PoolFactory' -export * from './OceanPool' +export * from './Pool' diff --git a/test/TestContractHandler.ts b/test/TestContractHandler.ts index ffc1143e..4ea9403b 100644 --- a/test/TestContractHandler.ts +++ b/test/TestContractHandler.ts @@ -2,8 +2,7 @@ import Web3 from 'web3' import { Contract } from 'web3-eth-contract' import { AbiItem } from 'web3-utils/types' import MockERC20 from '@oceanprotocol/contracts/artifacts/contracts/utils/mock/MockERC20Decimals.sol/MockERC20Decimals.json' -// TODO: add OPF deployment -const communityCollector = '0xeE9300b7961e0a01d9f0adb863C7A227A07AaD75' + const oceanAddress = '0x967da4048cd07ab37855c090aaf366e4ce1b9f48' export class TestContractHandler { public accounts: string[] @@ -29,6 +28,7 @@ export class TestContractHandler { public PoolTemplateBytecode: string public OPFCollectorBytecode: string public MockERC20Bytecode: string + public OPFBytecode: string public factory721Address: string public template721Address: string @@ -54,6 +54,7 @@ export class TestContractHandler { SideStakingABI?: AbiItem | AbiItem[], FixedRateABI?: AbiItem | AbiItem[], DispenserABI?: AbiItem | AbiItem[], + OPFABI?: AbiItem | AbiItem[], template721Bytecode?: string, template20Bytecode?: string, @@ -62,7 +63,8 @@ export class TestContractHandler { routerBytecode?: string, sideStakingBytecode?: string, fixedRateBytecode?: string, - dispenserBytecode?: string + dispenserBytecode?: string, + opfBytecode?: string ) { this.web3 = web3 this.ERC721Template = new this.web3.eth.Contract(ERC721TemplateABI) @@ -74,6 +76,7 @@ export class TestContractHandler { this.FixedRate = new this.web3.eth.Contract(FixedRateABI) this.Dispenser = new this.web3.eth.Contract(DispenserABI) this.MockERC20 = new this.web3.eth.Contract(MockERC20.abi as AbiItem[]) + this.OPFCollector = new this.web3.eth.Contract(OPFABI) this.ERC721FactoryBytecode = factory721Bytecode this.ERC20TemplateBytecode = template20Bytecode @@ -84,6 +87,7 @@ export class TestContractHandler { this.FixedRateBytecode = fixedRateBytecode this.DispenserBytecode = dispenserBytecode this.MockERC20Bytecode = MockERC20.bytecode + this.OPFBytecode = opfBytecode } public async getAccounts(): Promise { @@ -94,6 +98,29 @@ export class TestContractHandler { public async deployContracts(owner: string, routerABI?: AbiItem | AbiItem[]) { let estGas + // DEPLOY OPF Fee Collector + // get est gascost + estGas = await this.OPFCollector.deploy({ + data: this.OPFBytecode, + arguments: [owner, owner] + }).estimateGas(function (err, estGas) { + if (err) console.log('DeployContracts: ' + err) + return estGas + }) + // deploy the contract and get it's address + this.opfCollectorAddress = await this.OPFCollector.deploy({ + data: this.OPFBytecode, + arguments: [owner, owner] + }) + .send({ + from: owner, + gas: estGas + 1, + gasPrice: '3000000000' + }) + .then(function (contract) { + return contract.options.address + }) + // DEPLOY POOL TEMPLATE // get est gascost estGas = await this.PoolTemplate.deploy({ @@ -193,7 +220,7 @@ export class TestContractHandler { owner, this.oceanAddress, this.poolTemplateAddress, - communityCollector, + this.opfCollectorAddress, [] ] }).estimateGas(function (err, estGas) { @@ -207,7 +234,7 @@ export class TestContractHandler { owner, this.oceanAddress, this.poolTemplateAddress, - communityCollector, + this.opfCollectorAddress, [] ] }) @@ -245,7 +272,7 @@ export class TestContractHandler { // DEPLOY FIXED RATE estGas = await this.FixedRate.deploy({ data: this.FixedRateBytecode, - arguments: [this.routerAddress, communityCollector] + arguments: [this.routerAddress, this.opfCollectorAddress] }).estimateGas(function (err, estGas) { if (err) console.log('DeployContracts: ' + err) return estGas @@ -253,7 +280,7 @@ export class TestContractHandler { // deploy the contract and get it's address this.fixedRateAddress = await this.FixedRate.deploy({ data: this.FixedRateBytecode, - arguments: [this.routerAddress, communityCollector] + arguments: [this.routerAddress, this.opfCollectorAddress] }) .send({ from: owner, @@ -292,7 +319,7 @@ export class TestContractHandler { arguments: [ this.template721Address, this.template20Address, - communityCollector, + this.opfCollectorAddress, this.routerAddress ] }).estimateGas(function (err, estGas) { @@ -305,7 +332,7 @@ export class TestContractHandler { arguments: [ this.template721Address, this.template20Address, - communityCollector, + this.opfCollectorAddress, this.routerAddress ] }) @@ -378,7 +405,7 @@ export class TestContractHandler { .send({ from: owner }) // TODO: add OPF deployment // await RouterContract.methods - // .changeRouterOwner(communityCollector) + // .changeRouterOwner(this.opfCollectorAddress) // .send({ from: owner }) } } diff --git a/test/unit/pools/balancer/Pool.test.ts b/test/unit/pools/balancer/Pool.test.ts new file mode 100644 index 00000000..23d8f869 --- /dev/null +++ b/test/unit/pools/balancer/Pool.test.ts @@ -0,0 +1,1025 @@ +import { assert, expect } from 'chai' +import { AbiItem } from 'web3-utils/types' +import { TestContractHandler } from '../../../TestContractHandler' +import { Contract } from 'web3-eth-contract' +import Web3 from 'web3' +import BigNumber from 'bignumber.js' +import ERC721Factory from '@oceanprotocol/contracts/artifacts/contracts/ERC721Factory.sol/ERC721Factory.json' +import ERC721Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC721Template.sol/ERC721Template.json' +import SideStaking from '@oceanprotocol/contracts/artifacts/contracts/pools/ssContracts/SideStaking.sol/SideStaking.json' +import FactoryRouter from '@oceanprotocol/contracts/artifacts/contracts/pools/FactoryRouter.sol/FactoryRouter.json' +import ERC20Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20Template.sol/ERC20Template.json' +import Dispenser from '@oceanprotocol/contracts/artifacts/contracts/pools/dispenser/Dispenser.sol/Dispenser.json' +import FixedRate from '@oceanprotocol/contracts/artifacts/contracts/pools/fixedRate/FixedRateExchange.sol/FixedRateExchange.json' +import MockERC20 from '@oceanprotocol/contracts/artifacts/contracts/utils/mock/MockERC20Decimals.sol/MockERC20Decimals.json' +import PoolTemplate from '@oceanprotocol/contracts/artifacts/contracts/pools/balancer/BPool.sol/BPool.json' +import OPFCollector from '@oceanprotocol/contracts/artifacts/contracts/communityFee/OPFCommunityFeeCollector.sol/OPFCommunityFeeCollector.json' +import { LoggerInstance } from '../../../../src/utils' +import { NFTFactory } from '../../../../src/factories/NFTFactory' +import { Pool } from '../../../../src/pools/balancer/Pool' +const { keccak256 } = require('@ethersproject/keccak256') +const web3 = new Web3('http://127.0.0.1:8545') +const communityCollector = '0xeE9300b7961e0a01d9f0adb863C7A227A07AaD75' + +describe('Pool unit test', () => { + let factoryOwner: string + let nftOwner: string + let user1: string + let user2: string + let user3: string + let contracts: TestContractHandler + let pool: Pool + let dtAddress: string + let dtAddress2: string + let poolAddress: string + let erc20Token: string + let erc20Contract: Contract + let daiContract: Contract + let usdcContract: Contract + + it('should deploy contracts', async () => { + contracts = new TestContractHandler( + web3, + ERC721Template.abi as AbiItem[], + ERC20Template.abi as AbiItem[], + PoolTemplate.abi as AbiItem[], + ERC721Factory.abi as AbiItem[], + FactoryRouter.abi as AbiItem[], + SideStaking.abi as AbiItem[], + FixedRate.abi as AbiItem[], + Dispenser.abi as AbiItem[], + OPFCollector.abi as AbiItem[], + + ERC721Template.bytecode, + ERC20Template.bytecode, + PoolTemplate.bytecode, + ERC721Factory.bytecode, + FactoryRouter.bytecode, + SideStaking.bytecode, + FixedRate.bytecode, + Dispenser.bytecode, + OPFCollector.bytecode + ) + await contracts.getAccounts() + factoryOwner = contracts.accounts[0] + nftOwner = contracts.accounts[1] + user1 = contracts.accounts[2] + user2 = contracts.accounts[3] + user3 = contracts.accounts[4] + + await contracts.deployContracts(factoryOwner, FactoryRouter.abi as AbiItem[]) + + // initialize Pool instance + pool = new Pool(web3, LoggerInstance, PoolTemplate.abi as AbiItem[]) + assert(pool != null) + + daiContract = new web3.eth.Contract( + contracts.MockERC20.options.jsonInterface, + contracts.daiAddress + ) + + usdcContract = new web3.eth.Contract( + contracts.MockERC20.options.jsonInterface, + contracts.usdcAddress + ) + await pool.approve( + contracts.accounts[0], + contracts.daiAddress, + contracts.factory721Address, + '2000' + ) + await pool.approve( + contracts.accounts[0], + contracts.usdcAddress, + contracts.factory721Address, + '10000' + ) + console.log( + await pool.allowance( + contracts.daiAddress, + contracts.accounts[0], + contracts.factory721Address + ) + ) + expect( + await pool.allowance( + contracts.daiAddress, + contracts.accounts[0], + contracts.factory721Address + ) + ).to.equal('2000') + expect( + await pool.allowance( + contracts.usdcAddress, + contracts.accounts[0], + contracts.factory721Address + ) + ).to.equal('10000') + expect(await daiContract.methods.balanceOf(contracts.accounts[0]).call()).to.equal( + web3.utils.toWei('100000') + ) + + console.log( + await usdcContract.methods.decimals().call(), + 'USDC DECIMALS IN THIS TEST' + ) + }) + + describe('Test a pool with DAI (18 Decimals)', () => { + it('#create a pool', async () => { + // CREATE A POOL + // we prepare transaction parameters objects + const nftData = { + name: '72120Bundle', + symbol: '72Bundle', + templateIndex: 1, + baseURI: 'https://oceanprotocol.com/nft/' + } + const ercData = { + templateIndex: 1, + strings: ['ERC20B1', 'ERC20DT1Symbol'], + addresses: [ + contracts.accounts[0], + user3, + contracts.accounts[0], + '0x0000000000000000000000000000000000000000' + ], + uints: [web3.utils.toWei('1000000'), 0], + bytess: [] + } + + const basetokenInitialLiq = await pool.amountToUnits(contracts.daiAddress, '2000') + + const poolData = { + addresses: [ + contracts.sideStakingAddress, + contracts.daiAddress, + contracts.factory721Address, + contracts.accounts[0], + contracts.accounts[0], + contracts.poolTemplateAddress + ], + ssParams: [ + web3.utils.toWei('1'), // rate + 18, // basetokenDecimals + web3.utils.toWei('10000'), + 2500000, // vested blocks + web3.utils.toWei('2000') // baseToken initial pool liquidity + ], + swapFees: [ + 1e15, // + 1e15 + ] + } + + const nftFactory = new NFTFactory(contracts.factory721Address, web3, LoggerInstance) + + const txReceipt = await nftFactory.createNftErcWithPool( + contracts.accounts[0], + nftData, + ercData, + poolData + ) + + erc20Token = txReceipt.events.TokenCreated.returnValues.newTokenAddress + poolAddress = txReceipt.events.NewPool.returnValues.poolAddress + + erc20Contract = new web3.eth.Contract(ERC20Template.abi as AbiItem[], erc20Token) + // user2 has no dt1 + expect(await erc20Contract.methods.balanceOf(user2).call()).to.equal('0') + }) + + it('#sharesBalance - should return user shares balance (datatoken balance, LPT balance, etc) ', async () => { + expect(await daiContract.methods.balanceOf(user2).call()).to.equal( + await pool.sharesBalance(user2, contracts.daiAddress) + ) + }) + + it('#getNumTokens - should return num of tokens in pool (2)', async () => { + expect(await pool.getNumTokens(poolAddress)).to.equal('2') + }) + + it('#getPoolSharesTotalSupply - should return totalSupply of LPT', async () => { + // dt owner which added liquidity has half of pool shares (the rest is in the sidestaking contracta) + const dtOwnerLPTBalance = await pool.sharesBalance( + contracts.accounts[0], + poolAddress + ) + expect(await pool.sharesBalance(contracts.accounts[0], poolAddress)).to.equal( + await pool.sharesBalance(contracts.sideStakingAddress, poolAddress) + ) + // total supply is twice the dtOwner balance + expect(await pool.getPoolSharesTotalSupply(poolAddress)).to.equal( + (2 * Number(dtOwnerLPTBalance)).toString() + ) + }) + it('#getCurrentTokens - should return current pool tokens', async () => { + const currentTokens = await pool.getCurrentTokens(poolAddress) + expect(currentTokens[0]).to.equal(erc20Token) + expect(currentTokens[1]).to.equal(contracts.daiAddress) + }) + + it('#getFinalTokens - should return final pool tokens', async () => { + const finalTokens = await pool.getFinalTokens(poolAddress) + expect(finalTokens[0]).to.equal(erc20Token) + expect(finalTokens[1]).to.equal(contracts.daiAddress) + }) + + it('#getController - should return the pool controller (sideStaking address)', async () => { + expect(await pool.getController(poolAddress)).to.equal(contracts.sideStakingAddress) + }) + + it('#isBound - should return true if token is bound into the pool', async () => { + expect(await pool.isBound(poolAddress, contracts.daiAddress)).to.equal(true) + expect(await pool.isBound(poolAddress, contracts.oceanAddress)).to.equal(false) + }) + + it('#getReserve - should return final pool tokens', async () => { + expect(await pool.getReserve(poolAddress, contracts.daiAddress)).to.equal('2000') // base token initial liquidity + // rate is 1 so we have the same amount of DTs + expect(await pool.getReserve(poolAddress, erc20Token)).to.equal('2000') + }) + + it('#isFinalized - should return true if pool is finalized', async () => { + expect(await pool.isFinalized(poolAddress)).to.equal(true) + expect(await pool.isFinalized(contracts.oceanAddress)).to.equal(null) + }) + + it('#getSwapFee - should return the swap fee', async () => { + expect(await pool.getSwapFee(poolAddress)).to.equal('0.001') // 0.1% + }) + + it('#getNormalizedWeight - should return the normalized weight', async () => { + expect(await pool.getNormalizedWeight(poolAddress, contracts.daiAddress)).to.equal( + '0.5' + ) + expect(await pool.getNormalizedWeight(poolAddress, erc20Token)).to.equal('0.5') + }) + + it('#getDenormalizedWeight - should return the denormalized weight', async () => { + expect( + await pool.getDenormalizedWeight(poolAddress, contracts.daiAddress) + ).to.equal('5') + expect(await pool.getDenormalizedWeight(poolAddress, erc20Token)).to.equal('5') + }) + + it('#getBasetoken - should return the basetoken address', async () => { + expect(await pool.getBasetoken(poolAddress)).to.equal(contracts.daiAddress) + }) + + it('#getDatatoken - should return the datatoken address', async () => { + expect(await pool.getDatatoken(poolAddress)).to.equal(erc20Token) + }) + + it('#swapExactAmountIn - should swap', async () => { + await daiContract.methods + .transfer(user2, web3.utils.toWei('1000')) + .send({ from: contracts.accounts[0] }) + expect(await daiContract.methods.balanceOf(user2).call()).to.equal( + web3.utils.toWei('1000') + ) + expect(await erc20Contract.methods.balanceOf(user2).call()).to.equal('0') + await pool.approve(user2, contracts.daiAddress, poolAddress, '10') + const tx = await pool.swapExactAmountIn( + user2, + poolAddress, + contracts.daiAddress, + '10', + erc20Token, + '1' + ) + expect(await erc20Contract.methods.balanceOf(user2).call()).to.equal( + tx.events.LOG_SWAP.returnValues.tokenAmountOut + ) + }) + + it('#swapExactAmountOut - should swap', async () => { + await pool.approve(user2, contracts.daiAddress, poolAddress, '100') + expect(await daiContract.methods.balanceOf(user2).call()).to.equal( + web3.utils.toWei('990') + ) + const tx = await pool.swapExactAmountOut( + user2, + poolAddress, + contracts.daiAddress, + '100', + erc20Token, + '50' + ) + assert(tx != null) + }) + + it('#joinPool- user2 should add liquidity, receiving LP tokens', async () => { + const BPTAmountOut = '0.01' + const maxAmountsIn = [ + '50', // Amounts IN + '50' // Amounts IN + ] + + await pool.approve(user2, erc20Token, poolAddress, '50') + await pool.approve(user2, contracts.daiAddress, poolAddress, '50') + const tx = await pool.joinPool(user2, poolAddress, BPTAmountOut, maxAmountsIn) + assert(tx != null) + expect(await pool.sharesBalance(user2, poolAddress)).to.equal(BPTAmountOut) + expect(tx.events.LOG_JOIN.event === 'LOG_JOIN') + expect(tx.events.LOG_BPT.event === 'LOG_BPT') + }) + it('#joinswapExternAmountIn- user2 should add liquidity, receiving LP tokens', async () => { + const daiAmountIn = '100' + const minBPTOut = '0.1' + await pool.approve(user2, contracts.daiAddress, poolAddress, '100', true) + expect(await pool.allowance(contracts.daiAddress, user2, poolAddress)).to.equal( + '100' + ) + const tx = await pool.joinswapExternAmountIn( + user2, + poolAddress, + contracts.daiAddress, + daiAmountIn, + minBPTOut + ) + + assert(tx != null) + + expect(tx.events.LOG_JOIN[0].event === 'LOG_JOIN') + expect(tx.events.LOG_BPT.event === 'LOG_BPT') + // 2 JOIN EVENTS BECAUSE SIDE STAKING ALSO STAKED DTs, TODO: we should add to whom has been sent in the LOG_BPT event + expect(tx.events.LOG_JOIN[0].returnValues.bptAmount).to.equal( + tx.events.LOG_JOIN[1].returnValues.bptAmount + ) + }) + + it('#joinswapPoolAmountOut- user2 should add liquidity, receiving LP tokens', async () => { + const BPTAmountOut = '0.1' + const maxDAIIn = '100' + + await pool.approve(user2, contracts.daiAddress, poolAddress, '100') + + const tx = await pool.joinswapPoolAmountOut( + user2, + poolAddress, + contracts.daiAddress, + BPTAmountOut, + maxDAIIn + ) + + assert(tx != null) + + expect(tx.events.LOG_JOIN[0].event === 'LOG_JOIN') + expect(tx.events.LOG_BPT.event === 'LOG_BPT') + // 2 JOIN EVENTS BECAUSE SIDE STAKING ALSO STAKED DTs, TODO: we should add to whom has been sent in the LOG_BPT event + expect(tx.events.LOG_JOIN[0].returnValues.bptAmount).to.equal( + tx.events.LOG_JOIN[1].returnValues.bptAmount + ) + }) + + it('#exitPool- user2 exit the pool receiving both tokens, burning LP', async () => { + const BPTAmountIn = '0.5' + const minAmountOut = [ + '1', // min amount out for OCEAN AND DT + '1' + ] + + const tx = await pool.exitPool(user2, poolAddress, BPTAmountIn, minAmountOut) + + assert(tx != null) + + expect(tx.events.LOG_EXIT[0].returnValues.tokenOut).to.equal(erc20Token) + expect(tx.events.LOG_EXIT[1].returnValues.tokenOut).to.equal(contracts.daiAddress) + }) + + it('#exitswapPoolAmountIn- user2 exit the pool receiving only DAI', async () => { + const BPTAmountIn = '0.5' + const minDAIOut = '0.5' + + const tx = await pool.exitswapPoolAmountIn( + user2, + poolAddress, + contracts.daiAddress, + BPTAmountIn, + minDAIOut + ) + + assert(tx != null) + + expect(tx.events.LOG_EXIT[0].returnValues.tokenOut).to.equal(contracts.daiAddress) + + // DTs were also unstaked in the same transaction (went to the staking contract) + expect(tx.events.LOG_EXIT[1].returnValues.tokenOut).to.equal(erc20Token) + }) + + it('#exitswapExternAmountOut- user2 exit the pool receiving only DAI', async () => { + const maxBTPIn = '0.5' + const exactDAIOut = '1' + + const tx = await pool.exitswapPoolAmountIn( + user2, + poolAddress, + contracts.daiAddress, + maxBTPIn, + exactDAIOut + ) + + assert(tx != null) + + expect(tx.events.LOG_EXIT[0].returnValues.tokenOut).to.equal(contracts.daiAddress) + + // DTs were also unstaked in the same transaction (went to the staking contract) + expect(tx.events.LOG_EXIT[1].returnValues.tokenOut).to.equal(erc20Token) + }) + + it('#getAmountInExactOut- should get the amount in for exact out', async () => { + const maxBTPIn = '0.5' + const exactDAIOut = '1' + + const amountIn = await pool.getAmountInExactOut( + poolAddress, + erc20Token, + contracts.daiAddress, + exactDAIOut + ) + + assert(amountIn != null) + + // console.log(tx) + + const spotPrice = await pool.getSpotPrice( + poolAddress, + erc20Token, + contracts.daiAddress + ) + + // amount of DAI In will be slightly bigger than spotPrice + assert(amountIn > spotPrice) + }) + + it('#getAmountOutExactIn- should get the amount out for exact In', async () => { + const exactDTIn = '1' + + const amountOut = await pool.getAmountOutExactIn( + poolAddress, + erc20Token, + contracts.daiAddress, + exactDTIn + ) + + assert(amountOut != null) + + console.log(amountOut) + + const spotPrice = await pool.getSpotPrice( + poolAddress, + contracts.daiAddress, + erc20Token + ) + console.log(spotPrice) + // amount of DAI received will be slightly less than spotPrice + assert(amountOut < spotPrice) + }) + + it('#getSpotPrice- should get the spot price', async () => { + assert( + (await pool.getSpotPrice(poolAddress, erc20Token, contracts.daiAddress)) != null + ) + assert( + (await pool.getSpotPrice(poolAddress, contracts.daiAddress, erc20Token)) != null + ) + }) + + it('#getMarketFees- should get market fees for each token', async () => { + // we haven't performed any swap DT => DAI so there's no fee in erc20Token + // but there's a fee in DAI + assert((await pool.getMarketFees(poolAddress, erc20Token)) === '0') + assert((await pool.getMarketFees(poolAddress, contracts.daiAddress)) > '0') + }) + + it('#getCommunityFees- should get community fees for each token', async () => { + // we haven't performed any swap DT => DAI so there's no fee in erc20Token + // but there's a fee in DAI + + assert((await pool.getCommunityFees(poolAddress, erc20Token)) === '0') + assert((await pool.getCommunityFees(poolAddress, contracts.daiAddress)) > '0') + }) + + it('#collectMarketFee- should collect market fees for each token', async () => { + const spotPriceBefore = await pool.getSpotPrice( + poolAddress, + erc20Token, + contracts.daiAddress + ) + // contracts.accounts[0] is the marketFeeCollector + assert((await pool.getMarketFeeCollector(poolAddress)) === contracts.accounts[0]) + // user3 has no DAI (we are going to send DAI fee to him) + assert((await daiContract.methods.balanceOf(user3).call()) === '0') + // only marketFeeCollector can call this, set user3 as receiver + await pool.collectMarketFee(contracts.accounts[0], poolAddress, user3) + // DAI fees have been collected + assert((await pool.getMarketFees(poolAddress, contracts.daiAddress)) === '0') + // user3 got DAI + assert((await daiContract.methods.balanceOf(user3).call()) > '0') + // Spot price hasn't changed after fee collection + assert( + (await pool.getSpotPrice(poolAddress, erc20Token, contracts.daiAddress)) === + spotPriceBefore + ) + }) + + it('#getMarketFeeCollector- should get market fees for each token', async () => { + // contracts.accounts[0] is the marketFeeCollector + assert((await pool.getMarketFeeCollector(poolAddress)) === contracts.accounts[0]) + }) + + it('#getOPFCollector- should get market fees for each token', async () => { + assert((await pool.getOPFCollector(poolAddress)) === contracts.opfCollectorAddress) + }) + + it('#collectCommunityFee- should get community fees for each token', async () => { + const spotPriceBefore = await pool.getSpotPrice( + poolAddress, + erc20Token, + contracts.daiAddress + ) + // some fee are available in DAI + assert((await pool.getCommunityFees(poolAddress, contracts.daiAddress)) > '0') + // opf collector has no DAI + assert( + (await daiContract.methods.balanceOf(contracts.opfCollectorAddress).call()) === + '0' + ) + // anyone can call callectOPF + await pool.collectOPF(contracts.accounts[0], poolAddress) + // DAI fees have been collected + assert((await pool.getCommunityFees(poolAddress, contracts.daiAddress)) === '0') + // OPF collector got DAI + assert( + (await daiContract.methods.balanceOf(contracts.opfCollectorAddress).call()) > '0' + ) + // Spot price hasn't changed after fee collection + assert( + (await pool.getSpotPrice(poolAddress, erc20Token, contracts.daiAddress)) === + spotPriceBefore + ) + }) + + it('#updateMarketFeeCollector- should update market fee collector', async () => { + // contracts.accounts[0] is the marketFeeCollector + assert((await pool.getMarketFeeCollector(poolAddress)) === contracts.accounts[0]) + + await pool.updateMarketFeeCollector(contracts.accounts[0], poolAddress, user3) + + assert((await pool.getMarketFeeCollector(poolAddress)) === user3) + }) + }) + + describe('Test a pool with USDC (6 Decimals)', () => { + it('#create a pool', async () => { + // CREATE A POOL + // we prepare transaction parameters objects + const nftData = { + name: '72120Bundle', + symbol: '72Bundle', + templateIndex: 1, + baseURI: 'https://oceanprotocol.com/nft/' + } + const ercData = { + templateIndex: 1, + strings: ['ERC20B1', 'ERC20DT1Symbol'], + addresses: [ + contracts.accounts[0], + user3, + contracts.accounts[0], + '0x0000000000000000000000000000000000000000' + ], + uints: [web3.utils.toWei('1000000'), 0], + bytess: [] + } + const basetokenInitialLiq = Number( + await pool.amountToUnits(contracts.usdcAddress, '2000') + ) + console.log(basetokenInitialLiq.toString()) + const poolData = { + addresses: [ + contracts.sideStakingAddress, + contracts.usdcAddress, + contracts.factory721Address, + contracts.accounts[0], + contracts.accounts[0], + contracts.poolTemplateAddress + ], + ssParams: [ + web3.utils.toWei('1'), // rate + await usdcContract.methods.decimals().call(), // basetokenDecimals + web3.utils.toWei('10000'), + 2500000, // vested blocks + basetokenInitialLiq // baseToken initial pool liquidity + ], + swapFees: [ + 1e15, // + 1e15 + ] + } + + const nftFactory = new NFTFactory(contracts.factory721Address, web3, LoggerInstance) + + const txReceipt = await nftFactory.createNftErcWithPool( + contracts.accounts[0], + nftData, + ercData, + poolData + ) + + erc20Token = txReceipt.events.TokenCreated.returnValues.newTokenAddress + poolAddress = txReceipt.events.NewPool.returnValues.poolAddress + + erc20Contract = new web3.eth.Contract(ERC20Template.abi as AbiItem[], erc20Token) + // user2 has no dt1 + expect(await erc20Contract.methods.balanceOf(user2).call()).to.equal('0') + }) + + it('#sharesBalance - should return user shares balance (datatoken balance, LPT balance, etc) ', async () => { + expect(await usdcContract.methods.balanceOf(user2).call()).to.equal( + await pool.sharesBalance(user2, contracts.usdcAddress) + ) + }) + + it('#getNumTokens - should return num of tokens in pool (2)', async () => { + expect(await pool.getNumTokens(poolAddress)).to.equal('2') + }) + + it('#getPoolSharesTotalSupply - should return totalSupply of LPT', async () => { + // dt owner which added liquidity has half of pool shares (the rest is in the sidestaking contracta) + const dtOwnerLPTBalance = await pool.sharesBalance( + contracts.accounts[0], + poolAddress + ) + expect(await pool.sharesBalance(contracts.accounts[0], poolAddress)).to.equal( + await pool.sharesBalance(contracts.sideStakingAddress, poolAddress) + ) + // total supply is twice the dtOwner balance + expect(await pool.getPoolSharesTotalSupply(poolAddress)).to.equal( + (2 * Number(dtOwnerLPTBalance)).toString() + ) + }) + it('#getCurrentTokens - should return current pool tokens', async () => { + const currentTokens = await pool.getCurrentTokens(poolAddress) + expect(currentTokens[0]).to.equal(erc20Token) + expect(currentTokens[1]).to.equal(contracts.usdcAddress) + }) + + it('#getFinalTokens - should return final pool tokens', async () => { + const finalTokens = await pool.getFinalTokens(poolAddress) + expect(finalTokens[0]).to.equal(erc20Token) + expect(finalTokens[1]).to.equal(contracts.usdcAddress) + }) + + it('#getController - should return the pool controller (sideStaking address)', async () => { + expect(await pool.getController(poolAddress)).to.equal(contracts.sideStakingAddress) + }) + + it('#isBound - should return true if token is bound into the pool', async () => { + expect(await pool.isBound(poolAddress, contracts.usdcAddress)).to.equal(true) + expect(await pool.isBound(poolAddress, contracts.oceanAddress)).to.equal(false) + }) + + it('#getReserve - should return final pool tokens Reserve', async () => { + expect(await pool.getReserve(poolAddress, contracts.usdcAddress)).to.equal('2000') // base token initial liquidity + // rate is 1 so we have the same amount of DTs + expect(await pool.getReserve(poolAddress, erc20Token)).to.equal('2000') + }) + + it('#isFinalized - should return true if pool is finalized', async () => { + expect(await pool.isFinalized(poolAddress)).to.equal(true) + expect(await pool.isFinalized(contracts.oceanAddress)).to.equal(null) + }) + + it('#getSwapFee - should return the swap fee', async () => { + expect(await pool.getSwapFee(poolAddress)).to.equal('0.001') // 0.1% + }) + + it('#getNormalizedWeight - should return the normalized weight', async () => { + expect(await pool.getNormalizedWeight(poolAddress, contracts.usdcAddress)).to.equal( + '0.5' + ) + expect(await pool.getNormalizedWeight(poolAddress, erc20Token)).to.equal('0.5') + }) + + it('#getDenormalizedWeight - should return the denormalized weight', async () => { + expect( + await pool.getDenormalizedWeight(poolAddress, contracts.usdcAddress) + ).to.equal('5') + expect(await pool.getDenormalizedWeight(poolAddress, erc20Token)).to.equal('5') + }) + + it('#getBasetoken - should return the basetoken address', async () => { + expect(await pool.getBasetoken(poolAddress)).to.equal(contracts.usdcAddress) + }) + + it('#getDatatoken - should return the datatoken address', async () => { + expect(await pool.getDatatoken(poolAddress)).to.equal(erc20Token) + }) + + it('#swapExactAmountIn - should swap', async () => { + const transferAmount = await pool.amountToUnits(contracts.usdcAddress, '1000') // 1000 USDC + await usdcContract.methods + .transfer(user2, transferAmount) + .send({ from: contracts.accounts[0] }) + expect(await usdcContract.methods.balanceOf(user2).call()).to.equal( + transferAmount.toString() + ) + + expect(await erc20Contract.methods.balanceOf(user2).call()).to.equal('0') + await pool.approve(user2, contracts.usdcAddress, poolAddress, '10') + const tx = await pool.swapExactAmountIn( + user2, + poolAddress, + contracts.usdcAddress, + '10', + erc20Token, + '1' + ) + expect(await erc20Contract.methods.balanceOf(user2).call()).to.equal( + tx.events.LOG_SWAP.returnValues.tokenAmountOut + ) + }) + + it('#swapExactAmountOut - should swap', async () => { + expect(await usdcContract.methods.balanceOf(user2).call()).to.equal( + (await pool.amountToUnits(contracts.usdcAddress, '990')).toString() + ) + await pool.approve(user2, contracts.usdcAddress, poolAddress, '100') + const tx = await pool.swapExactAmountOut( + user2, + poolAddress, + contracts.usdcAddress, + '100', + erc20Token, + '50' + ) + assert(tx != null) + // console.log(tx.events) + }) + + it('#joinPool- user2 should add liquidity, receiving LP tokens', async () => { + const BPTAmountOut = '0.01' + const maxAmountsIn = [ + '50', // Amounts IN + '50' // Amounts IN + ] + + await pool.approve(user2, erc20Token, poolAddress, '50') + await pool.approve(user2, contracts.usdcAddress, poolAddress, '50') + const tx = await pool.joinPool(user2, poolAddress, BPTAmountOut, maxAmountsIn) + assert(tx != null) + expect(await pool.sharesBalance(user2, poolAddress)).to.equal(BPTAmountOut) + expect(tx.events.LOG_JOIN.event === 'LOG_JOIN') + expect(tx.events.LOG_BPT.event === 'LOG_BPT') + + // console.log(tx) + // console.log(tx.events.LOG_JOIN) + // console.log(tx.events.LOG_BPT) + }) + it('#joinswapExternAmountIn- user2 should add liquidity, receiving LP tokens', async () => { + const usdcAmountIn = '100' + const minBPTOut = '0.1' + await pool.approve(user2, contracts.usdcAddress, poolAddress, '100', true) + + const tx = await pool.joinswapExternAmountIn( + user2, + poolAddress, + contracts.usdcAddress, + usdcAmountIn, + minBPTOut + ) + + assert(tx != null) + + expect(tx.events.LOG_JOIN[0].event === 'LOG_JOIN') + expect(tx.events.LOG_BPT.event === 'LOG_BPT') + // 2 JOIN EVENTS BECAUSE SIDE STAKING ALSO STAKED DTs, TODO: we should add to whom has been sent in the LOG_BPT event + expect(tx.events.LOG_JOIN[0].returnValues.bptAmount).to.equal( + tx.events.LOG_JOIN[1].returnValues.bptAmount + ) + }) + + it('#joinswapPoolAmountOut- user2 should add liquidity, receiving LP tokens', async () => { + const BPTAmountOut = '0.1' + const maxUSDCIn = '100' + + await pool.approve(user2, contracts.usdcAddress, poolAddress, '100') + + const tx = await pool.joinswapPoolAmountOut( + user2, + poolAddress, + contracts.usdcAddress, + BPTAmountOut, + maxUSDCIn + ) + + assert(tx != null) + + expect(tx.events.LOG_JOIN[0].event === 'LOG_JOIN') + expect(tx.events.LOG_BPT.event === 'LOG_BPT') + // 2 JOIN EVENTS BECAUSE SIDE STAKING ALSO STAKED DTs, TODO: we should add to whom has been sent in the LOG_BPT event + expect(tx.events.LOG_JOIN[0].returnValues.bptAmount).to.equal( + tx.events.LOG_JOIN[1].returnValues.bptAmount + ) + }) + + it('#exitPool- user2 exit the pool receiving both tokens, burning LP', async () => { + const BPTAmountIn = '0.5' + const minAmountOut = [ + '1', // min amount out for USDC AND DT + '1' + ] + + const tx = await pool.exitPool(user2, poolAddress, BPTAmountIn, minAmountOut) + + assert(tx != null) + + expect(tx.events.LOG_EXIT[0].returnValues.tokenOut).to.equal(erc20Token) + expect(tx.events.LOG_EXIT[1].returnValues.tokenOut).to.equal(contracts.usdcAddress) + }) + + it('#exitswapPoolAmountIn- user2 exit the pool receiving only USDC', async () => { + const BPTAmountIn = '0.5' + const minUSDCOut = '0.5' + + const tx = await pool.exitswapPoolAmountIn( + user2, + poolAddress, + contracts.usdcAddress, + BPTAmountIn, + minUSDCOut + ) + + assert(tx != null) + + expect(tx.events.LOG_EXIT[0].returnValues.tokenOut).to.equal(contracts.usdcAddress) + + // DTs were also unstaked in the same transaction (went to the staking contract) + expect(tx.events.LOG_EXIT[1].returnValues.tokenOut).to.equal(erc20Token) + }) + + it('#exitswapExternAmountOut- user2 exit the pool receiving only USDC', async () => { + const maxBTPIn = '0.5' + const exactUSDCOut = '1' + + const tx = await pool.exitswapPoolAmountIn( + user2, + poolAddress, + contracts.usdcAddress, + maxBTPIn, + exactUSDCOut + ) + + assert(tx != null) + + expect(tx.events.LOG_EXIT[0].returnValues.tokenOut).to.equal(contracts.usdcAddress) + + // DTs were also unstaked in the same transaction (went to the staking contract) + expect(tx.events.LOG_EXIT[1].returnValues.tokenOut).to.equal(erc20Token) + }) + + it('#getAmountInExactOut- should get the amount in for exact out', async () => { + const maxBTPIn = '0.5' + const exactUSDCOut = '1' + + const amountIn = await pool.getAmountInExactOut( + poolAddress, + erc20Token, + contracts.usdcAddress, + exactUSDCOut + ) + + assert(amountIn != null) + + console.log(amountIn.toString()) + + const spotPrice = await pool.getSpotPrice( + poolAddress, + erc20Token, + contracts.usdcAddress + ) + console.log(spotPrice.toString(), 'spotPrice') + // amount of USDC In will be slightly bigger than spotPrice + assert(amountIn > spotPrice) + }) + + it('#getAmountOutExactIn- should get the amount out for exact In', async () => { + const exactDTIn = '1' + + const amountOut = await pool.getAmountOutExactIn( + poolAddress, + erc20Token, + contracts.usdcAddress, + exactDTIn + ) + + assert(amountOut != null) + + console.log(amountOut) + + const spotPrice = await pool.getSpotPrice( + poolAddress, + contracts.usdcAddress, + erc20Token + ) + console.log(spotPrice, 'spotPrice') + // amount of USDC received will be slightly less than spotPrice + assert(amountOut < spotPrice) + }) + + it('#getSpotPrice- should get the spot price', async () => { + assert( + (await pool.getSpotPrice(poolAddress, erc20Token, contracts.usdcAddress)) != null + ) + assert( + (await pool.getSpotPrice(poolAddress, contracts.usdcAddress, erc20Token)) != null + ) + }) + + it('#getMarketFees- should get market fees for each token', async () => { + // we haven't performed any swap DT => USDC so there's no fee in erc20Token + // but there's a fee in USDC + assert((await pool.getMarketFees(poolAddress, erc20Token)) === '0') + assert((await pool.getMarketFees(poolAddress, contracts.usdcAddress)) > '0') + }) + + it('#getCommunityFees- should get community fees for each token', async () => { + // we haven't performed any swap DT => USDC so there's no fee in erc20Token + // but there's a fee in USDC + + assert((await pool.getCommunityFees(poolAddress, erc20Token)) === '0') + assert((await pool.getCommunityFees(poolAddress, contracts.usdcAddress)) > '0') + }) + + it('#collectMarketFee- should collect market fees for each token', async () => { + const spotPriceBefore = await pool.getSpotPrice( + poolAddress, + erc20Token, + contracts.usdcAddress + ) + // contracts.accounts[0] is the marketFeeCollector + assert((await pool.getMarketFeeCollector(poolAddress)) === contracts.accounts[0]) + // user3 has no USDC (we are going to send USDC fee to him) + assert((await usdcContract.methods.balanceOf(user3).call()) === '0') + // only marketFeeCollector can call this, set user3 as receiver + await pool.collectMarketFee(contracts.accounts[0], poolAddress, user3) + // USDC fees have been collected + assert((await pool.getMarketFees(poolAddress, contracts.usdcAddress)) === '0') + // user3 got USDC + assert((await usdcContract.methods.balanceOf(user3).call()) > '0') + // Spot price hasn't changed after fee collection + assert( + (await pool.getSpotPrice(poolAddress, erc20Token, contracts.usdcAddress)) === + spotPriceBefore + ) + }) + + it('#getMarketFeeCollector- should get market fees for each token', async () => { + // contracts.accounts[0] is the marketFeeCollector + assert((await pool.getMarketFeeCollector(poolAddress)) === contracts.accounts[0]) + }) + + it('#getOPFCollector- should get market fees for each token', async () => { + assert((await pool.getOPFCollector(poolAddress)) === contracts.opfCollectorAddress) + }) + + it('#collectCommunityFee- should get community fees for each token', async () => { + const spotPriceBefore = await pool.getSpotPrice( + poolAddress, + erc20Token, + contracts.usdcAddress + ) + // some fee are available in USDC + assert((await pool.getCommunityFees(poolAddress, contracts.usdcAddress)) > '0') + // opf collector has no USDC + assert( + (await usdcContract.methods.balanceOf(contracts.opfCollectorAddress).call()) === + '0' + ) + // anyone can call callectOPF + await pool.collectOPF(contracts.accounts[0], poolAddress) + // USDC fees have been collected + assert((await pool.getCommunityFees(poolAddress, contracts.usdcAddress)) === '0') + // OPF collector got USDC + assert( + (await usdcContract.methods.balanceOf(contracts.opfCollectorAddress).call()) > '0' + ) + // Spot price hasn't changed after fee collection + assert( + (await pool.getSpotPrice(poolAddress, erc20Token, contracts.usdcAddress)) === + spotPriceBefore + ) + }) + + it('#updateMarketFeeCollector- should update market fee collector', async () => { + // contracts.accounts[0] is the marketFeeCollector + assert((await pool.getMarketFeeCollector(poolAddress)) === contracts.accounts[0]) + + await pool.updateMarketFeeCollector(contracts.accounts[0], poolAddress, user3) + + assert((await pool.getMarketFeeCollector(poolAddress)) === user3) + }) + }) +})