diff --git a/package.json b/package.json index 17f31e9c..6d3062a4 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "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/balancer/Pool.test.ts'", + "test:ss": "mocha --config=test/unit/.mocharc.json --node-env=test --exit 'test/unit/pools/ssContracts/SideStaking.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", diff --git a/src/pools/balancer/Pool.ts b/src/pools/balancer/Pool.ts index d6a5585f..e61abd45 100644 --- a/src/pools/balancer/Pool.ts +++ b/src/pools/balancer/Pool.ts @@ -785,7 +785,7 @@ export class Pool { minAmountOutFormatted.toString(), maxPrice ? this.web3.utils.toWei(maxPrice) : MaxUint256 ) - console.log(minAmountOutFormatted, 'minamoutnoutformatted') + //console.log(minAmountOutFormatted, 'minamoutnoutformatted') try { result = await pool.methods .swapExactAmountIn( diff --git a/src/pools/ssContracts/SideStaking.ts b/src/pools/ssContracts/SideStaking.ts new file mode 100644 index 00000000..01da4fc2 --- /dev/null +++ b/src/pools/ssContracts/SideStaking.ts @@ -0,0 +1,490 @@ +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 SideStakingTemplate from '@oceanprotocol/contracts/artifacts/contracts/pools/ssContracts/SideStaking.sol/SideStaking.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 SideStaking { + public ssABI: AbiItem | AbiItem[] + public web3: Web3 + public GASLIMIT_DEFAULT = 1000000 + private logger: Logger + + constructor(web3: Web3, logger: Logger, ssABI: AbiItem | AbiItem[] = null) { + if (ssABI) this.ssABI = ssABI + else this.ssABI = SideStakingTemplate.abi as AbiItem[] + this.web3 = web3 + this.logger = logger + } + + 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() + } + + /** + * 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 + * @param {String} ssAddress side staking contract address + * @return {String} + */ + async getDataTokenCirculatingSupply( + ssAddress: string, + datatokenAddress: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods + .getDataTokenCirculatingSupply(datatokenAddress) + .call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @return {String} + */ + async getPublisherAddress( + ssAddress: string, + datatokenAddress: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getPublisherAddress(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {String} + */ + async getBasetoken(ssAddress: string, datatokenAddress: string): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getBaseToken(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {String} + */ + async getPoolAddress(ssAddress: string, datatokenAddress: string): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getPoolAddress(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {String} + */ + async getBasetokenBalance( + ssAddress: string, + datatokenAddress: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getBasetokenBalance(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {String} + */ + async getDatatokenBalance( + ssAddress: string, + datatokenAddress: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getDatatokenBalance(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {String} + */ + async getvestingEndBlock(ssAddress: string, datatokenAddress: string): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getvestingEndBlock(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {String} + */ + async getvestingAmount(ssAddress: string, datatokenAddress: string): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getvestingAmount(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {String} + */ + async getvestingLastBlock( + ssAddress: string, + datatokenAddress: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getvestingLastBlock(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {String} + */ + async getvestingAmountSoFar( + ssAddress: string, + datatokenAddress: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods.getvestingAmountSoFar(datatokenAddress).call() + } catch (e) { + this.logger.error(`ERROR: Failed to get: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @param {String} amount amount of DTs we want to Side Staking to stake + * @return {String} + */ + async canStake( + ssAddress: string, + datatokenAddress: string, + amount: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods + .canStake( + datatokenAddress, + await this.getBasetoken(ssAddress, datatokenAddress), + this.amountToUnits(datatokenAddress, amount) + ) + .call() + } catch (e) { + this.logger.error(`ERROR: Failed to get if can stake DT: ${e.message}`) + } + return result + } + + /** + * Get + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @param {String} amount amount of LPT we want to Side Staking to unstake + * @return {String} + */ + async canUnStake( + ssAddress: string, + datatokenAddress: string, + amount: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + try { + result = await sideStaking.methods + .canUnStake( + datatokenAddress, + await this.getBasetoken(ssAddress, datatokenAddress), + this.amountToUnits( + await this.getPoolAddress(ssAddress, datatokenAddress), + amount + ) + ) + .call() + } catch (e) { + this.logger.error(`ERROR: Failed to get if can stake DT: ${e.message}`) + } + return result + } + + /** + * Estimate gas cost for collectMarketFee + * @param {String} account + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @param {Contract} contractInstance optional contract instance + * @return {Promise} + */ + public async estGetVesting( + account: string, + ssAddress: string, + datatokenAddress: string, + contractInstance?: Contract + ): Promise { + const sideStaking = + contractInstance || new this.web3.eth.Contract(this.ssABI as AbiItem[], ssAddress) + + const gasLimitDefault = this.GASLIMIT_DEFAULT + let estGas + try { + estGas = await sideStaking.methods + .getVesting(datatokenAddress) + .estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas)) + } catch (e) { + estGas = gasLimitDefault + } + return estGas + } + + /** + * + * @param {String} account + * @param {String} ssAddress side staking contract address + * @param {String} datatokenAddress datatokenAddress + * @return {TransactionReceipt} + */ + async getVesting( + account: string, + ssAddress: string, + datatokenAddress: string + ): Promise { + const sideStaking = new this.web3.eth.Contract(this.ssABI, ssAddress) + let result = null + + const estGas = await this.estGetVesting( + account, + ssAddress, + datatokenAddress, + sideStaking + ) + + try { + result = await sideStaking.methods.getVesting(datatokenAddress).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 + } +} diff --git a/src/pools/ssContracts/index.ts b/src/pools/ssContracts/index.ts new file mode 100644 index 00000000..8ade0ddb --- /dev/null +++ b/src/pools/ssContracts/index.ts @@ -0,0 +1 @@ +export * from './SideStaking' diff --git a/test/unit/pools/balancer/Pool.test.ts b/test/unit/pools/balancer/Pool.test.ts index 23d8f869..5462dc4e 100644 --- a/test/unit/pools/balancer/Pool.test.ts +++ b/test/unit/pools/balancer/Pool.test.ts @@ -123,6 +123,8 @@ describe('Pool unit test', () => { await usdcContract.methods.decimals().call(), 'USDC DECIMALS IN THIS TEST' ) + + await pool.amountToUnits(contracts.usdcAddress, '20') }) describe('Test a pool with DAI (18 Decimals)', () => { diff --git a/test/unit/pools/ssContracts/SideStaking.test.ts b/test/unit/pools/ssContracts/SideStaking.test.ts new file mode 100644 index 00000000..d7c94869 --- /dev/null +++ b/test/unit/pools/ssContracts/SideStaking.test.ts @@ -0,0 +1,520 @@ +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 SSContract 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' +import { SideStaking } from '../../../../src/pools/ssContracts/SideStaking' +const { keccak256 } = require('@ethersproject/keccak256') +const web3 = new Web3('http://127.0.0.1:8545') +const communityCollector = '0xeE9300b7961e0a01d9f0adb863C7A227A07AaD75' + +describe('SideStaking unit test', () => { + let factoryOwner: string + let nftOwner: string + let user1: string + let user2: string + let user3: string + let contracts: TestContractHandler + let pool: Pool + let sideStaking: SideStaking + 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[], + SSContract.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, + SSContract.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) + // + sideStaking = new SideStaking(web3, LoggerInstance, SSContract.abi as AbiItem[]) + assert(sideStaking != 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' + ) + + 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' + ) + + await pool.amountToUnits(contracts.usdcAddress, '20') + }) + + 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('#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('#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('#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) + }) + + + }) + + 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') + ) + + 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('#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('#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('#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) + }) + + + }) +})