diff --git a/.gitignore b/.gitignore index c296647..077f8e5 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ solidity/build/Test* /rbtcwrapperproxy/private-key /rbtcwrapperproxy/build -workspace.code-workspace +*.code-workspace .env yarn.lock diff --git a/ADD-POOLS.md b/ADD-POOLS.md index 5c76fdc..e54fa84 100644 --- a/ADD-POOLS.md +++ b/ADD-POOLS.md @@ -80,12 +80,29 @@ The configuration file is updated during the process, in order to allow resuming ### Adding LM pools to the testnet and mainnet (only needed if contracts' abi were changed) -1. Before adding pools we need to make sure having updated comipled contracts in [./solidity/build](./solidity/build) - - Change contract folder in script Compile contracts `../../scripts/compile.sh` - - Run from root dir `./scripts/compile.sh` -2. Top up RBTC and WRBTC account with some RBTC to pay for tx and **WRBTC** amount for initial deposit (0.01 usually) +1. Use ```yarn install``` or ```npm install``` from the root to install node modules +2. Before adding pools we need to make sure having updated compiled contracts in [./solidity/build/contracts](./solidity/build/contracts) + - Run from root dir ```truffle compile``` +3. Top up deploying account with RBTC to pay tx fees and pair tokens for initial deposit if it is not zero - at least `balance` amounts in config file, e.g. 18 XUSD and 100 BRZ: + ```js + "reserves": [ + { + "symbol": "XUSD", + "weight": "50%", + "balance": "18" + }, + { + "symbol": "BRZ", + "weight": "50%", + "balance": "100" + } + ], + "k": 6779, + "btcAddress": "0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab", + ``` + * For WRBTC-paired tokens **WRBTC** amount for initial deposit is usually 0.01 -To add converters to an existing swap network, use ```addConverter.js```. +To add converters to an existing swap network, use ```addConverter.js``` or ```addConverterNoOracle.js```. Example creation v1 pool WRBTC/ETH @@ -93,11 +110,17 @@ Note: `data_testnet.json` and `data_mainnet.json` store contracts addresses part testnet: -```node addConverter.js ETH addETHs_testnet.json data_testnet.json https://public-node.testnet.rsk.co ``` +```node addConverter.js ETH addETHs_testnet.json data_testnet.json https://public-node.testnet.rsk.co ``` + +for pairs with no WRBTC (with no internal oracle): + +```node addConverterNoOracle.js BRZ addBRZ_testnet.json data_testnet.json https://public-node.testnet.rsk.co ``` mainnet: -```node addConverter.js ETH addETHs_mainnet.json data_mainnet.json https://mainnet.sovryn.app/rpc ``` +```node addConverter.js ETH addETHs_mainnet.json data_mainnet.json https://mainnet2.sovryn.app/rpc ``` + or respectively +```node addConverterNoOracle.js BRZ addBRZ_testnet.json data_testnet.json https://public-node.testnet.rsk.co ``` ```TODO: check if the following is for v2 pools only``` diff --git a/hardhat.config.js b/hardhat.config.js index e92f8ce..bc25ea2 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -7,19 +7,19 @@ require("@nomiclabs/hardhat-web3"); require("hardhat-contract-sizer"); //yarn run hardhat size-contracts require("solidity-coverage"); // $ npx hardhat coverage require("hardhat-log-remover"); -require('hardhat-docgen'); +require("hardhat-docgen"); // This is a sample Hardhat task. To learn how to create your own go to // https://hardhat.org/guides/create-task.html /// this is for use with ethers.js task("accounts", "Prints the list of accounts", async () => { - const accounts = await ethers.getSigners(); + const accounts = await ethers.getSigners(); - for (const account of accounts.address) { - const wallet = ethers.Wallet.fromMnemonic("test test test test test test test test test test test junk", "m/44'/60'/0'/0"); + for (const account of accounts.address) { + const wallet = ethers.Wallet.fromMnemonic("test test test test test test test test test test test junk", "m/44'/60'/0'/0"); - console.log(account); - } + console.log(account); + } }); /*task("accounts", "Prints accounts", async (_, { web3 }) => { @@ -36,61 +36,61 @@ task("accounts", "Prints the list of accounts", async () => { /**/ module.exports = { - solidity: { - version: "0.4.26", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - contractSizer: { - alphaSort: false, - runOnCompile: false, - disambiguatePaths: false, - }, - networks: { - hardhat: {}, - rskPublicTestnet: { - url: "https://public-node.testnet.rsk.co/", - accounts: { mnemonic: "brownie", count: 10 }, - network_id: 31, - confirmations: 4, - gasMultiplier: 1.25, - //timeout: 20000, // increase if needed; 20000 is the default value - //allowUnlimitedContractSize, //EIP170 contrtact size restriction temporal testnet workaround - }, - rskPublicMainnet: { - url: "https://public-node.rsk.co/", - network_id: 30, - //timeout: 20000, // increase if needed; 20000 is the default value - }, - rskSovrynTestnet: { - url: "https://testnet.sovryn.app/rpc", - accounts: { mnemonic: "brownie", count: 10 }, - network_id: 31, - confirmations: 4, - gasMultiplier: 1.25, - //timeout: 20000, // increase if needed; 20000 is the default value - //allowUnlimitedContractSize, //EIP170 contrtact size restriction temporal testnet workaround - }, - rskSovrynMainnet: { - url: "https://mainnet.sovryn.app/rpc", - network_id: 30, - //timeout: 20000, // increase if needed; 20000 is the default value - }, - }, - paths: { - sources: "./solidity/contracts", - tests: "./solidity/test/", - }, - mocha: { - timeout: 800000, - grep: "^(?!.*; using Ganache).*", - }, - docgen: { - path: './docs', - clear: true - } -}; \ No newline at end of file + solidity: { + version: "0.4.26", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + contractSizer: { + alphaSort: false, + runOnCompile: false, + disambiguatePaths: false, + }, + networks: { + hardhat: {}, + rskPublicTestnet: { + url: "https://public-node.testnet.rsk.co/", + accounts: { mnemonic: "brownie", count: 10 }, + network_id: 31, + confirmations: 4, + gasMultiplier: 1.25, + //timeout: 20000, // increase if needed; 20000 is the default value + //allowUnlimitedContractSize, //EIP170 contrtact size restriction temporal testnet workaround + }, + rskPublicMainnet: { + url: "https://public-node.rsk.co/", + network_id: 30, + //timeout: 20000, // increase if needed; 20000 is the default value + }, + rskSovrynTestnet: { + url: "https://testnet.sovryn.app/rpc", + accounts: { mnemonic: "brownie", count: 10 }, + network_id: 31, + confirmations: 4, + gasMultiplier: 1.25, + //timeout: 20000, // increase if needed; 20000 is the default value + //allowUnlimitedContractSize, //EIP170 contrtact size restriction temporal testnet workaround + }, + rskSovrynMainnet: { + url: "https://mainnet2.sovryn.app/rpc", + network_id: 30, + //timeout: 20000, // increase if needed; 20000 is the default value + }, + }, + paths: { + sources: "./solidity/contracts", + tests: "./solidity/test/", + }, + mocha: { + timeout: 800000, + grep: "^(?!.*; using Ganache).*", + }, + docgen: { + path: "./docs", + clear: true, + }, +}; diff --git a/rbtcwrapperproxy/contracts/RBTCWrapperProxy.sol b/rbtcwrapperproxy/contracts/RBTCWrapperProxy.sol index 83ce4bb..17ec75c 100644 --- a/rbtcwrapperproxy/contracts/RBTCWrapperProxy.sol +++ b/rbtcwrapperproxy/contracts/RBTCWrapperProxy.sol @@ -12,7 +12,6 @@ import "./interfaces/ISovrynSwapFormula.sol"; import "./interfaces/IContractRegistry.sol"; import "./ContractRegistryClient.sol"; import "./mockups/LiquidityMining.sol"; -import "./interfaces/ILoanToken.sol"; contract RBTCWrapperProxy is ContractRegistryClient { @@ -181,7 +180,7 @@ contract RBTCWrapperProxy is ContractRegistryClient { IWrbtcERC20(wrbtcTokenAddress).deposit.value(_amount)(); } else{ - reserveToken.transferFrom(msg.sender, address(this), _amount); + require(reserveToken.transferFrom(msg.sender, address(this), _amount)); } require(reserveToken.approve(_liquidityPoolConverterAddress, _amount), "token approval failed"); @@ -455,46 +454,4 @@ contract RBTCWrapperProxy is ContractRegistryClient { } } - /** - * @notice provides funds to a lending pool and deposits the pool tokens into the liquidity mining contract. - * @param loanTokenAddress the address of the loan token (aka lending pool) - * @param depositAmount he amount of underlying tokens to deposit - */ - function addToLendingPool(address loanTokenAddress, uint256 depositAmount) public{ - LoanToken loanToken = LoanToken(loanTokenAddress); - IERC20Token underlyingAsset = IERC20Token(loanToken.loanTokenAddress()); - - //retrieve the underlying asset from the user - require(underlyingAsset.transferFrom(msg.sender, address(this), depositAmount), "Failed to transfer tokens to the wrapper proxy"); - - //add the tokens to the lending pool - underlyingAsset.approve(loanTokenAddress, depositAmount); - uint256 minted = loanToken.mint(address(this), depositAmount); - - //deposit the pool tokens in the liquidity mining contract on the sender's behalf - loanToken.approve(address(liquidityMiningContract), minted); - liquidityMiningContract.deposit(loanTokenAddress, minted, msg.sender); - - emit LoanTokensMinted(msg.sender, minted, depositAmount); - } - - /** - * @notice removes funds from the liquidity mining contract, burns them on the lending pool and - * provides the underlying asset to the user - * @param loanTokenAddress the address of the loan token (aka lending pool) - * @param burnAmount the amount of pool tokens to withdraw from the lending pool and burn - */ - function removeFromLendingPool(address loanTokenAddress, uint256 burnAmount) public{ - LoanToken loanToken = LoanToken(loanTokenAddress); - - //withdraw always transfers the pool tokens to the caller and the reward tokens to the passed address - liquidityMiningContract.withdraw(loanTokenAddress, burnAmount, msg.sender); - - //burn pool token and directly send underlying tokens to the receiver - loanToken.approve(address(liquidityMiningContract), burnAmount); - uint256 redeemed = loanToken.burn(msg.sender, burnAmount); - - emit LoanTokensBurnt(msg.sender, burnAmount, redeemed); - } - } \ No newline at end of file diff --git a/rbtcwrapperproxy/contracts/interfaces/ILoanToken.sol b/rbtcwrapperproxy/contracts/interfaces/ILoanToken.sol deleted file mode 100644 index e1fbcff..0000000 --- a/rbtcwrapperproxy/contracts/interfaces/ILoanToken.sol +++ /dev/null @@ -1,28 +0,0 @@ -pragma solidity 0.5.16; - -import "./IERC20Token.sol"; - -/** -This is just a mockup for the Loan Token contract in the Sovryn-smart-contracts repository - */ -contract LoanToken is IERC20Token{ - address public loanTokenAddress; - - /** - * @notice lend to the pool - * @param _amount the amount of underlying tokens - * @param _user the address of user, tokens will be deposited to it or to msg.sender - */ - function mint(address _user, uint256 _amount) public returns (uint256 mintAmount){ - - } - - /** - * @notice burns pool tokens and transfers underlying tokens - * @param _user the address of the receiver - * @param _amount the amount of pool tokens - */ - function burn(address _user, uint256 _amount) public returns (uint256 redeemedAmount){ - - } -} \ No newline at end of file diff --git a/rbtcwrapperproxy/contracts/mockups/LoanToken.sol b/rbtcwrapperproxy/contracts/mockups/LoanToken.sol deleted file mode 100644 index 14bd3ea..0000000 --- a/rbtcwrapperproxy/contracts/mockups/LoanToken.sol +++ /dev/null @@ -1,94 +0,0 @@ -pragma solidity 0.5.16; - -import "../interfaces/IERC20Token.sol"; - -/** -This is just a mockup for the Loan Token contract in the Sovryn-smart-contracts repository - */ -contract LoanToken{ - address public loanTokenAddress; - - mapping( address => uint) public balanceOf; - mapping( address => mapping( address => uint)) public allowance; - - constructor(address loanToken) public{ - loanTokenAddress = loanToken; - } - /** - * @notice lend to the pool - * @param _amount the amount of underlying tokens - * @param _user the address of user, tokens will be deposited to it or to msg.sender - */ - function mint(address _user, uint256 _amount) public returns (uint256 mintAmount){ - IERC20Token(loanTokenAddress).transferFrom(address(msg.sender), address(this), _amount); - balanceOf[_user] += _amount; - return _amount; - } - - /** - * @notice burns pool tokens and transfers underlying tokens - * @param _receiver the address of the receiver - * @param _amount the amount of pool tokens - */ - function burn(address _receiver, uint256 _amount) public returns (uint256 redeemedAmount){ - require(balanceOf[msg.sender] >= _amount, "insufficient balance"); - balanceOf[msg.sender] -= _amount; - IERC20Token(loanTokenAddress).transfer(address(_receiver), _amount); - return _amount; - } - - /** - * @dev allows another account/contract to transfers tokens on behalf of the caller - * throws on any error rather then return a false flag to minimize user errors - * - * also, to minimize the risk of the approve/transferFrom attack vector - * (see https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/), approve has to be called twice - * in 2 separate transactions - once to change the allowance to 0 and secondly to change it to the new allowance value - * - * @param _spender approved address - * @param _value allowance amount - * - * @return true if the approval was successful, false if it wasn't - */ - function approve(address _spender, uint256 _value) public returns (bool success) { - allowance[msg.sender][_spender] = _value; - return true; - } - - /** - * @dev transfers tokens to a given address on behalf of another address - * throws on any error rather then return a false flag to minimize user errors - * - * @param _from source address - * @param _to target address - * @param _value transfer amount - * - * @return true if the transfer was successful, false if it wasn't - */ - function transferFrom( - address _from, - address _to, - uint256 _value - ) public returns (bool success) { - require(allowance[_from][msg.sender] >= _value); - allowance[_from][msg.sender] = allowance[_from][msg.sender] - _value; - balanceOf[_from] = balanceOf[_from] - _value; - balanceOf[_to] = balanceOf[_to] + _value; - return true; - } - - /** - * @dev transfers tokens to a given address - * throws on any error rather then return a false flag to minimize user errors - * - * @param _to target address - * @param _value transfer amount - * - * @return true if the transfer was successful, false if it wasn't - */ - function transfer(address _to, uint256 _value) public returns (bool success) { - balanceOf[msg.sender] = balanceOf[msg.sender] - _value; - balanceOf[_to] = balanceOf[_to] + _value; - return true; - } -} \ No newline at end of file diff --git a/rbtcwrapperproxy/migrations/2_deploy_RBTCWrapperProxy.js b/rbtcwrapperproxy/migrations/2_deploy_RBTCWrapperProxy.js index 340c0c7..40a7027 100644 --- a/rbtcwrapperproxy/migrations/2_deploy_RBTCWrapperProxy.js +++ b/rbtcwrapperproxy/migrations/2_deploy_RBTCWrapperProxy.js @@ -2,7 +2,6 @@ const fs = require("fs"); const RBTCWrapperProxy = artifacts.require("RBTCWrapperProxy"); const LiquidityMining = artifacts.require("LiquidityMining"); -const LoanToken = artifacts.require("LoanToken"); const getConfig = () => { return JSON.parse(fs.readFileSync("../solidity/utils/config_rsk.json", { encoding: "utf8" })); @@ -14,20 +13,26 @@ const getSOVConfig = () => { module.exports = function (deployer, network) { if(network == "development"){ console.log(getConfig()["SUSD"].addr) - return deployer.deploy(LoanToken, getConfig()["SUSD"].addr).then(function() { - return deployer.deploy(LiquidityMining, getSOVConfig()["SOV"].addr).then(function() { - return deployer.deploy(RBTCWrapperProxy, getConfig()["RBTC"].addr, getConfig()["sovrynSwapNetwork"].addr, getConfig()["contractRegistry"].addr, LiquidityMining.address); - }); + return deployer.deploy(LiquidityMining, getSOVConfig()["SOV"].addr).then(function() { + return deployer.deploy(RBTCWrapperProxy, getConfig()["RBTC"].addr, getConfig()["sovrynSwapNetwork"].addr, getConfig()["contractRegistry"].addr, LiquidityMining.address); }); } - else{ - liquidityMiningAddress = '0xe28aEbA913c34EC8F10DF0D9C92D2Aa27545870e'; - wrbtcAddress = '0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab'; - swapNetworkAddress = '0x61172B53423E205a399640e5283e51FE60EC2256'; - contractRegistry = '0x0E7CcF6A67e614B507Aa524572F72C7e5Dec23CB'; + else{//mainnet + liquidityMiningAddress = '0xf730af26e87D9F55E46A6C447ED2235C385E55e0'; + wrbtcAddress = '0x542fDA317318eBF1d3DEAf76E0b632741A7e677d'; + swapNetworkAddress = '0x98aCE08D2b759a265ae326F010496bcD63C15afc'; + contractRegistry = '0x46EBC03EF2277308BdB106a73d11c65109C4B89B'; return deployer.deploy(RBTCWrapperProxy, wrbtcAddress, swapNetworkAddress, contractRegistry, liquidityMiningAddress); } + //testnet + /** + liquidityMiningAddress = '0xe28aEbA913c34EC8F10DF0D9C92D2Aa27545870e'; + wrbtcAddress = '0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab'; + swapNetworkAddress = '0x61172B53423E205a399640e5283e51FE60EC2256'; + contractRegistry = '0x0E7CcF6A67e614B507Aa524572F72C7e5Dec23CB'; + */ + }; diff --git a/rbtcwrapperproxy/test/testRBTCWrapperProxy.js b/rbtcwrapperproxy/test/testRBTCWrapperProxy.js index 631d148..81ed0b5 100644 --- a/rbtcwrapperproxy/test/testRBTCWrapperProxy.js +++ b/rbtcwrapperproxy/test/testRBTCWrapperProxy.js @@ -50,7 +50,6 @@ contract("RBTCWrapperProxy", async (accounts) => { usdToken = await IERC20Token.at(usdTokenAddress); sovrynSwapNetwork = await ISovrynSwapNetwork.at(sovrynSwapNetworkAddress); liquidityMining = await LiquidityMining.deployed(); - usdLoanToken = await LoanToken.deployed(); await sovToken.transfer(liquidityMining.address, web3.utils.toWei("100","Ether")); }); @@ -400,457 +399,4 @@ contract("RBTCWrapperProxy", async (accounts) => { ); }); - it("should add to the lending pool and provide the pool tokens to the LM contract", async() => { - const amount = web3.utils.toBN(10e18); - const usdBefore = await usdToken.balanceOf(accounts[0]); - await usdToken.approve(rbtcWrapperProxy.address, amount); - await rbtcWrapperProxy.addToLendingPool(usdLoanToken.address, amount); - const usdAfter = await usdToken.balanceOf(accounts[0]); - assert.equal(amount.add(usdAfter).toString(), usdBefore.toString(), "incorrect account token balance"); - assert.equal(amount.toString(), (await usdToken.balanceOf(usdLoanToken.address)).toString(), "incorrect underlying balance on loan token"); - assert.equal(amount.toString(), (await usdLoanToken.balanceOf(liquidityMining.address)).toString(), "incorrect pool token balance on LM contract"); - }); - - require("@openzeppelin/test-helpers/configure")({ - provider: web3.currentProvider, - singletons: { - abstraction: "truffle", - }, -}); -const fs = require("fs"); - -const assert = require("chai").assert; -const { expectRevert, expectEvent, BN } = require("@openzeppelin/test-helpers"); - -const RBTCWrapperProxy = artifacts.require("../RBTCWrapperProxy.sol"); -const IERC20Token = artifacts.require("../interfaces/IERC20Token.sol"); -const ISmartToken = artifacts.require("../interfaces/ISmartToken.sol"); -const ISovrynSwapNetwork = artifacts.require("../interfaces/ISovrynSwapNetwork.sol"); -const ILiquidityPoolV1Converter = artifacts.require("../interfaces/ILiquidityPoolV1Converter.sol"); -const ILiquidityPoolV2Converter = artifacts.require("../interfaces/ILiquidityPoolV2Converter.sol"); -const LiquidityMining = artifacts.require("../mockups/LiquidityMining.sol"); -const LoanToken = artifacts.require("../mockups/LoanToken.sol"); - -const getConfig = () => { - return JSON.parse(fs.readFileSync("../solidity/utils/config_rsk.json", { encoding: "utf8" })); -}; - -const getConfigFromSOV = () => { - return JSON.parse(fs.readFileSync("../solidity/utils/addSOV.json", { encoding: "utf8" })); -}; - -const liquidityPoolV1ConverterAddress = getConfigFromSOV()["newLiquidityPoolV1Converter"].addr; -const liquidityPoolV2ConverterAddress = getConfig()["newLiquidityPoolV2Converter"].addr; -const sovrynSwapNetworkAddress = getConfig()["sovrynSwapNetwork"].addr; -const wrbtcAddress = getConfig()["RBTC"].addr; -const sovTokenAddress = getConfigFromSOV()["SOV"].addr; -const usdTokenAddress = getConfig()["SUSD"].addr; - -contract("RBTCWrapperProxy", async (accounts) => { - let rbtcWrapperProxy, liquidityPoolV1Converter, liquidityPoolV2Converter, poolTokenV1Address, wrbtcPoolTokenV2Address, poolTokenV1, wrbtcPoolTokenV2, sovToken, usdToken, sovrynSwapNetwork; - - before(async () => { - rbtcWrapperProxy = await RBTCWrapperProxy.deployed(); - liquidityPoolV1Converter = await ILiquidityPoolV1Converter.at(liquidityPoolV1ConverterAddress); - liquidityPoolV2Converter = await ILiquidityPoolV2Converter.at(liquidityPoolV2ConverterAddress); - poolTokenV1Address = await liquidityPoolV1Converter.token(); - poolTokenV1 = await ISmartToken.at(poolTokenV1Address); - wrbtcPoolTokenV2Address = await liquidityPoolV2Converter.poolToken(wrbtcAddress); - wrbtcPoolTokenV2 = await ISmartToken.at(wrbtcPoolTokenV2Address); - usdPoolTokenV2Address = await liquidityPoolV2Converter.poolToken(usdTokenAddress); - usdPoolTokenV2 = await ISmartToken.at(usdPoolTokenV2Address); - sovToken = await IERC20Token.at(sovTokenAddress); - usdToken = await IERC20Token.at(usdTokenAddress); - sovrynSwapNetwork = await ISovrynSwapNetwork.at(sovrynSwapNetworkAddress); - liquidityMining = await LiquidityMining.deployed(); - usdLoanToken = await LoanToken.deployed(); - await sovToken.transfer(liquidityMining.address, web3.utils.toWei("100","Ether")); - }); - - it("verifies that users could send RBTC and SOV, and then add liquidity to get pool token 1", async () => { - var sovAmountBefore = await sovToken.balanceOf(accounts[0]); - var poolTokenV1AmountBefore = await poolTokenV1.balanceOf(accounts[0]); - - var rbtcAmount = web3.utils.toBN(getConfigFromSOV()["converters"][0]["reserves"][0]["balance"] * 1e14); - var sovAmount = web3.utils.toBN(getConfigFromSOV()["converters"][0]["reserves"][1]["balance"] * 1e14); - - await sovToken.approve(RBTCWrapperProxy.address, web3.utils.toBN(sovAmount * 4), { from: accounts[0] }); - - var result = await rbtcWrapperProxy.addLiquidityToV1( - liquidityPoolV1ConverterAddress, - [wrbtcAddress,sovTokenAddress], - [rbtcAmount, sovAmount], - 1, - { - from: accounts[0], - to: RBTCWrapperProxy.address, - value: rbtcAmount - }); - - var addedPoolTokenV1Amount = new BN(result.logs[0].args._poolTokenAmount); - - var expectedBalance = new BN(sovAmountBefore.toString()).sub(sovAmount) - assert.equal( - await sovToken.balanceOf(accounts[0]), - expectedBalance.toString(), - "Wrong SOV balance" - ); - - assert.equal( - (await liquidityMining.userLPBalance(accounts[0], poolTokenV1Address)).toString(), - addedPoolTokenV1Amount.toString(), - "Wrong pool token balance on LM contract" - ); - - await expectEvent(result.receipt, "LiquidityAddedToV1", { - _provider: accounts[0], - _reserveTokens: [wrbtcAddress, sovTokenAddress], - _reserveAmounts: [rbtcAmount, sovAmount], - _poolTokenAmount: addedPoolTokenV1Amount, - }); - - var sovAmountAfter = await sovToken.balanceOf(accounts[0]); - - // Send 2x SOV, User should get 1x SOV back - var result = await rbtcWrapperProxy.addLiquidityToV1( - liquidityPoolV1ConverterAddress, - [wrbtcAddress,sovTokenAddress], - [rbtcAmount, web3.utils.toBN(sovAmount * 2)], - 1, - { - from: accounts[0], - to: RBTCWrapperProxy.address, - value: rbtcAmount - }); - - addedPoolTokenV1Amount = addedPoolTokenV1Amount.add(new BN(result.logs[0].args._poolTokenAmount)); - expectedBalance = new BN(sovAmountAfter.toString()).sub(sovAmount); - - assert.equal( - await sovToken.balanceOf(accounts[0]), - expectedBalance.toString(), - "Wrong SOV balance" - ); - - assert.equal( - (await liquidityMining.userLPBalance(accounts[0], poolTokenV1Address)).toString(), - addedPoolTokenV1Amount.toString(), - "Wrong pool token balance on LM contract" - ); - - await expectEvent(result.receipt, "LiquidityAddedToV1", { - _provider: accounts[0], - _reserveTokens: [wrbtcAddress, sovTokenAddress], - _reserveAmounts: [rbtcAmount, web3.utils.toBN(sovAmount * 2)] - }); - }); - - it("verifies that users could remove liquidity to burn pool token 1 and then get RBTC and SOV", async () => { - poolTokenBalance = await liquidityMining.userLPBalance(accounts[0], poolTokenV1Address); - assert(poolTokenBalance > 0, "incorrect test setup"); - - rbtcAmountBefore = await web3.eth.getBalance(accounts[0]); - sovAmountBefore = await sovToken.balanceOf(accounts[0]); - - var result = await rbtcWrapperProxy.removeLiquidityFromV1(liquidityPoolV1ConverterAddress, poolTokenBalance, [wrbtcAddress, sovTokenAddress], [1, 1]); - - var gasCost = new BN(result.receipt.gasUsed).mul( new BN((await web3.eth.getGasPrice()).toString())); - var addedRBTCAmount = result.logs[0].args._reserveAmounts[0]; - var addedSOVAmount = result.logs[0].args._reserveAmounts[1]; - - await expectEvent(result.receipt, "LiquidityRemovedFromV1", { - _provider: accounts[0], - _reserveTokens: [wrbtcAddress, sovTokenAddress], - _reserveAmounts: [addedRBTCAmount, addedSOVAmount] - }); - - expectedBalance = new BN(rbtcAmountBefore).add(new BN(addedRBTCAmount)).sub(gasCost); - assert.equal( - await web3.eth.getBalance(accounts[0]), - expectedBalance.toString(), - "Wrong RBTC balance" - ); - - //adding the pool token balance because of the reward being paid out - expectedBalance = new BN(sovAmountBefore).add(addedSOVAmount).add(poolTokenBalance); - assert.equal( - (await sovToken.balanceOf(accounts[0])).toString(), - expectedBalance.toString(), - "Wrong SOV balance" - ); - }); - - it("verifies that users could send RBTC and then add liquidity to get pool token 2", async () => { - var rbtcAmountBefore = await web3.eth.getBalance(accounts[0]); - var wrbtcPoolTokenV2AmountBefore = await wrbtcPoolTokenV2.balanceOf(accounts[0]); - - var result = await rbtcWrapperProxy.addLiquidityToV2(liquidityPoolV2ConverterAddress, wrbtcAddress, web3.utils.toBN(1e16), 1, { - from: accounts[0], - to: RBTCWrapperProxy.address, - value: 1e16, - }); - - var gasCost = new BN(result.receipt.gasUsed).mul( new BN((await web3.eth.getGasPrice()).toString())); - var addedWrbtcPoolTokenV2Amount = new BN(result.logs[0].args._poolTokenAmount); - var expectedBalance = new BN(rbtcAmountBefore.toString()).sub(gasCost).sub(new BN((10**16).toString())); - - assert.equal(await web3.eth.getBalance(accounts[0]), expectedBalance, "Wrong RBTC balance"); - - assert.equal( - (await liquidityMining.userLPBalance(accounts[0], wrbtcPoolTokenV2Address)).toString(), - addedWrbtcPoolTokenV2Amount.toString(), - "Wrong pool token balance on LM contract" - ); - await expectEvent(result.receipt, "LiquidityAdded", { - _provider: accounts[0], - _reserveAmount: web3.utils.toBN(1e16), - _poolTokenAmount: addedWrbtcPoolTokenV2Amount, - }); - }); - - it("verifies that users could add USD liquidity to the v2 pool", async () => { - var usdAmountBefore = await usdToken.balanceOf(accounts[0]); - - await usdToken.approve(RBTCWrapperProxy.address, web3.utils.toBN(1e16), { from: accounts[0] }); - - var result = await rbtcWrapperProxy.addLiquidityToV2(liquidityPoolV2ConverterAddress, usdTokenAddress, web3.utils.toBN(1e16), 1); - - var addedUsdPoolTokenV2Amount = new BN(result.logs[0].args._poolTokenAmount); - var expectedBalance = usdAmountBefore.sub(new BN((10**16).toString())); - - assert.equal((await usdToken.balanceOf(accounts[0])).toString(), expectedBalance.toString(), "Wrong USD balance"); - - assert.equal( - (await liquidityMining.userLPBalance(accounts[0], usdPoolTokenV2Address)).toString(), - addedUsdPoolTokenV2Amount.toString(), - "Wrong pool token balance" - ); - await expectEvent(result.receipt, "LiquidityAdded", { - _provider: accounts[0], - _reserveAmount: web3.utils.toBN(1e16), - _poolTokenAmount: addedUsdPoolTokenV2Amount, - }); - }); - - it("verifies that users could remove liquidity to burn pool token 2 and then get RBTC", async () => { - poolTokenBalance = await liquidityMining.userLPBalance(accounts[0], wrbtcPoolTokenV2Address); - assert(poolTokenBalance > 0, "incorrect test setup"); - - var sovAmountBefore = await sovToken.balanceOf(accounts[0]); - - var rbtcAmountBefore = await web3.eth.getBalance(accounts[0]); - var result = await rbtcWrapperProxy.removeLiquidityFromV2(liquidityPoolV2ConverterAddress, wrbtcAddress, poolTokenBalance, 1, { - from: accounts[0], - to: RBTCWrapperProxy.address, - }); - - var gasCost = new BN(result.receipt.gasUsed).mul( new BN((await web3.eth.getGasPrice()).toString())); - var addedRBTCAmount = web3.utils.BN(result.logs[0].args._reserveAmount); - var expectedBalance = new BN(rbtcAmountBefore).add(addedRBTCAmount).sub(gasCost); - - assert.equal( - await web3.eth.getBalance(accounts[0]), - expectedBalance.toString(), - "Wrong RBTC balance" - ); - - //checking if the LM reward being paid out - expectedBalance = new BN(sovAmountBefore).add(poolTokenBalance); - assert.equal( - (await sovToken.balanceOf(accounts[0])).toString(), - expectedBalance.toString(), - "Wrong SOV balance" - ); - - await expectEvent(result.receipt, "LiquidityRemoved", { - _provider: accounts[0], - _reserveAmount: addedRBTCAmount, - _poolTokenAmount: poolTokenBalance, - }); - }); - - it("verifies that users could remove liquidity to burn pool token 2 and then get RBTC", async () => { - poolTokenBalance = await liquidityMining.userLPBalance(accounts[0], usdPoolTokenV2Address); - assert(poolTokenBalance > 0, "incorrect test setup"); - - var sovAmountBefore = await sovToken.balanceOf(accounts[0]); - - var usdAmountBefore = await usdToken.balanceOf(accounts[0]); - var result = await rbtcWrapperProxy.removeLiquidityFromV2(liquidityPoolV2ConverterAddress, usdTokenAddress, poolTokenBalance, 1, { - from: accounts[0], - to: RBTCWrapperProxy.address, - }); - - var addedUSDAmount = web3.utils.BN(result.logs[0].args._reserveAmount); - var expectedBalance = usdAmountBefore.add(addedUSDAmount); - - assert.equal( - await usdToken.balanceOf(accounts[0]), - expectedBalance.toString(), - "Wrong USD balance" - ); - - //checking if the LM reward being paid out - expectedBalance = new BN(sovAmountBefore).add(poolTokenBalance); - assert.equal( - (await sovToken.balanceOf(accounts[0])).toString(), - expectedBalance.toString(), - "Wrong SOV balance" - ); - - await expectEvent(result.receipt, "LiquidityRemoved", { - _provider: accounts[0], - _reserveAmount: addedUSDAmount, - _poolTokenAmount: poolTokenBalance, - }); - }); - - it("verifies that users could send RBTC and then swap it to DoC", async () => { - var rbtcAmountBefore = await web3.eth.getBalance(accounts[0]); - var usdTokenAmountBefore = await usdToken.balanceOf(accounts[0]); - - var pathWRBTCToDoC = await sovrynSwapNetwork.conversionPath(wrbtcAddress, usdTokenAddress); - var result = await rbtcWrapperProxy.convertByPath(pathWRBTCToDoC, web3.utils.toBN(1e16), 1, { - from: accounts[0], - to: RBTCWrapperProxy.address, - value: 1e16 - }); - - var gasCost = new BN(result.receipt.gasUsed).mul(new BN((await web3.eth.getGasPrice()).toString())); - var addedDoCTokenAmount = web3.utils.BN(result.logs[0].args._targetTokenAmount); - var expectedBalance = new BN(rbtcAmountBefore).sub(gasCost).sub(new BN((10**16).toString())); - assert.equal(await web3.eth.getBalance(accounts[0]), expectedBalance.toString(), "Wrong RBTC balance"); - expectedBalance = new BN(usdTokenAmountBefore).add(addedDoCTokenAmount); - assert.equal( - await usdToken.balanceOf(accounts[0]), - expectedBalance.toString(), - "Wrong DoC token balance" - ); - await expectEvent(result.receipt, "TokenConverted", { - _beneficiary: accounts[0], - _sourceTokenAmount: web3.utils.toBN(1e16), - _targetTokenAmount: addedDoCTokenAmount, - _path: pathWRBTCToDoC, - }); - }); - - it("verifies that users could send DoC and then swap it to RBTC", async () => { - var rbtcAmountBefore = await web3.eth.getBalance(accounts[0]); - var usdTokenAmountBefore = await usdToken.balanceOf(accounts[0]); - - await usdToken.approve(RBTCWrapperProxy.address, web3.utils.toBN(1e20), { from: accounts[0] }); - - var pathDoCToWRBTC = await sovrynSwapNetwork.conversionPath(usdTokenAddress, wrbtcAddress); - var rbtcAmountBefore = await web3.eth.getBalance(accounts[0]); - - var result = await rbtcWrapperProxy.convertByPath(pathDoCToWRBTC, web3.utils.toBN(1e20), 1, { - from: accounts[0], - to: RBTCWrapperProxy.address, - }); - - var gasCost = new BN(result.receipt.gasUsed).mul( new BN((await web3.eth.getGasPrice()).toString()));; - var addedRBTCAmount = new BN(result.logs[0].args._targetTokenAmount); - var expectedBalance = new BN(rbtcAmountBefore).add(addedRBTCAmount).sub(gasCost); - assert.equal( - await web3.eth.getBalance(accounts[0]), - expectedBalance.toString(), - "Wrong RBTC balance" - ); - - expectedBalance = new BN(usdTokenAmountBefore).sub(web3.utils.toBN(1e20)); - assert.equal( - web3.utils.BN(await usdToken.balanceOf(accounts[0])).toString(), - expectedBalance.toString(), - "Wrong DoC token balance" - ); - await expectEvent(result.receipt, "TokenConverted", { - _beneficiary: accounts[0], - _sourceTokenAmount: web3.utils.toBN(1e20), - _targetTokenAmount: addedRBTCAmount, - _path: pathDoCToWRBTC, - }); - }); - - it("should revert when sending rBTC to this smart contract from user directly", async () => { - await expectRevert.unspecified(rbtcWrapperProxy.send(web3.utils.toBN(1e16))); - }); - - it("should revert when calling the addLIquidity() without sending RBTC", async () => { - await expectRevert.unspecified( - rbtcWrapperProxy.addLiquidityToV2(liquidityPoolV2ConverterAddress, wrbtcAddress, web3.utils.toBN(1e16), 1, { - from: accounts[0], - to: RBTCWrapperProxy.address, - }), - "No RBTC" - ); - }); - - it("should revert when amount param not identical to msg.value to call addLiquidityToV2()", async () => { - await expectRevert.unspecified( - rbtcWrapperProxy.addLiquidityToV2(liquidityPoolV2ConverterAddress, wrbtcAddress, web3.utils.toBN(1e16), 1, { - from: accounts[0], - to: RBTCWrapperProxy.address, - value: 1e8, - }), - "The provided amount not identical to msg.value" - ); - }); - - it("should revert when passing wrong path param to convertByPath()", async () => { - var pathWRBTCToDoC = await sovrynSwapNetwork.conversionPath(wrbtcAddress, usdTokenAddress); - var pathDoCToWRBTC = await sovrynSwapNetwork.conversionPath(usdTokenAddress, wrbtcAddress); - await expectRevert.unspecified( - rbtcWrapperProxy.convertByPath(pathDoCToWRBTC, web3.utils.toBN(1e16), 1, { - from: accounts[0], - to: RBTCWrapperProxy.address, - value: 1e16, - }), - "Wrong path param" - ); - await expectRevert.unspecified( - rbtcWrapperProxy.convertByPath(pathWRBTCToDoC, web3.utils.toBN(1e20), 1, { from: accounts[0], to: RBTCWrapperProxy.address }), - "Wrong path param" - ); - }); - - it("should add to the lending pool and provide the pool tokens to the LM contract", async() => { - const amount = web3.utils.toBN(10e18); - const usdBefore = await usdToken.balanceOf(accounts[0]); - await usdToken.approve(rbtcWrapperProxy.address, amount); - const result = await rbtcWrapperProxy.addToLendingPool(usdLoanToken.address, amount); - const usdAfter = await usdToken.balanceOf(accounts[0]); - assert.equal(amount.add(usdAfter).toString(), usdBefore.toString(), "incorrect account token balance"); - assert.equal(amount.toString(), (await usdToken.balanceOf(usdLoanToken.address)).toString(), "incorrect underlying balance on loan token"); - assert.equal(amount.toString(), (await usdLoanToken.balanceOf(liquidityMining.address)).toString(), "incorrect pool token balance on LM contract"); - - expectEvent(result.receipt, "LoanTokensMinted", { - user: accounts[0], - poolTokenAmount: amount, - assetAmount: amount - }); - }); - - it("should withdraw from the lending pool and withdraw the rewrads from the LM contract", async() => { - const amount = await liquidityMining.userLPBalance(accounts[0], usdLoanToken.address); - assert(amount > 0, "incorrect test setup. account needs to have a balance on the LM contract"); - - const usdBefore = await usdToken.balanceOf(accounts[0]); - const sovBefore = await sovToken.balanceOf(accounts[0]); - const result = await rbtcWrapperProxy.removeFromLendingPool(usdLoanToken.address, amount); - - assert.equal(amount.add(usdBefore).toString(), (await usdToken.balanceOf(accounts[0])).toString(), "incorrect account token balance"); - assert.equal(amount.add(sovBefore).toString(), (await sovToken.balanceOf(accounts[0])).toString(), "incorrect account token balance"); - - expectEvent(result.receipt, "LoanTokensBurnt", { - user: accounts[0], - poolTokenAmount: amount, - assetAmount: amount - }); - }); - - -}); - - - }); diff --git a/rbtcwrapperproxy/truffle-config.js b/rbtcwrapperproxy/truffle-config.js index 430c67e..9dec249 100644 --- a/rbtcwrapperproxy/truffle-config.js +++ b/rbtcwrapperproxy/truffle-config.js @@ -59,7 +59,7 @@ module.exports = { // options below to some value. // testnet: { - provider: () => new HDWalletProvider(privateKey, "wss://testnet.sovryn.app/ws"), + provider: () => new HDWalletProvider(privateKey, "wss://testnet.sovryn.app/websocket"), network_id: 31, gasPrice: 95000010, skipDryRun: true, diff --git a/solidity/utils/addBRZ_testnet.json b/solidity/utils/addBRZ_testnet.json new file mode 100644 index 0000000..323d801 --- /dev/null +++ b/solidity/utils/addBRZ_testnet.json @@ -0,0 +1,51 @@ +{ + "reserves": [ + { + "symbol": "XUSD", + "decimals": 18, + "address": "0xa9262CC3fB54Ea55B1B0af00EfCa9416B8d59570" + }, + { + "symbol": "BRZ", + "decimals": 4, + "address": "0xe355c280131dfaf18bf1c3648aee3c396db6b5fd" + } + ], + "converters": [ + { + "type": 1, + "symbol": "XUSD/BRZ", + "decimals": 18, + "fee": "0.3%", + "reserves": [ + { + "symbol": "XUSD", + "weight": "50%", + "balance": "100" + }, + { + "symbol": "BRZ", + "weight": "50%", + "balance": "495" + } + ] + } + ], + "ALERT": "THIS PAIR IS NOT wRBTC BASED - NO INTERNAL ORACLE - 'K' AND 'btcAddress below are not used", + "k": 6779, + "btcAddress": "0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab", + "phase": 7, + "XUSD": { + "name": "XUSD", + "addr": "0xa9262CC3fB54Ea55B1B0af00EfCa9416B8d59570" + }, + "BRZ": { + "name": "BRZ", + "addr": "0xe355c280131dfaf18bf1c3648aee3c396db6b5fd" + }, + "newLiquidityPoolV1Converter": { + "name": "LiquidityPoolV1Converter", + "addr": "0x3378E4dc28c862E71E9c097C12305513B3cCc6B9", + "args": "" + } +} \ No newline at end of file diff --git a/solidity/utils/addConverter.js b/solidity/utils/addConverter.js index 5ec4f05..de90c1b 100644 --- a/solidity/utils/addConverter.js +++ b/solidity/utils/addConverter.js @@ -3,15 +3,15 @@ const path = require("path"); const Web3 = require("web3"); const TOKEN_NAME = process.argv[2]; -const NETWORK = process.argv[3]; +const TOKEN_CONFIG_FILENAME = process.argv[3]; +const DATA_FILENAME = process.argv[4]; const NODE_ADDRESS = process.argv[5]; const PRIVATE_KEY = process.argv[6]; -const ARTIFACTS_DIR = path.resolve(__dirname, "../build"); +const ARTIFACTS_DIR = path.resolve(__dirname, "../build/contracts"); const MIN_GAS_LIMIT = 100000; -const TOKEN_CONFIG_FILENAME = `config_${NETWORK}_${TOKEN_NAME}.json`; -const DATA_FILENAME = `add_${TOKEN_NAME}.json`; + String.prototype.replaceAll = function (exp, newStr) { return this.replace(new RegExp(exp, "gm"), newStr); }; @@ -35,25 +35,19 @@ String.prototype.format = function (args) { return result; }; -let web3; -let gasPrice; -let account; -let phase = 0; - -const initialiseWeb3 = async () => { - const nodeURL = NODE_ADDRESS; - const privateKey = PRIVATE_KEY; - web3 = new Web3(nodeURL); - account = await web3.eth.accounts.privateKeyToAccount(privateKey); - gasPrice = await getGasPrice(web3); -} - const getConfig = () => { + //return JSON.parse(fs.readFileSync(path.join(__dirname, "add{token}.json".format({ token: TOKEN_NAME })), { encoding: "utf8" })); return JSON.parse(fs.readFileSync(path.join(__dirname, TOKEN_CONFIG_FILENAME), { encoding: "utf8" })); }; +const getData = () => { + //TODO read the config according to the network; temporarily moved to input params + //return JSON.parse(fs.readFileSync("./config_rsk_testnet.json", { encoding: "utf8" })); + return JSON.parse(fs.readFileSync(DATA_FILENAME, { encoding: "utf8" })); +}; + const setConfig = (record) => { - fs.writeFileSync(path.join(__dirname, DATA_FILENAME), JSON.stringify({ ...getConfig(), ...record }, null, 4)); + fs.writeFileSync(path.join(__dirname, TOKEN_CONFIG_FILENAME), JSON.stringify({ ...getConfig(), ...record }, null, 4)); }; const scan = async (message) => { @@ -125,15 +119,24 @@ const send = async (web3, account, gasPrice, transaction, value = 0) => { }; const deploy = async (web3, account, gasPrice, contractId, contractName, contractArgs) => { - if (getConfig()[contractId] === undefined) { + if (getConfig()[contractId] === undefined || contractId === "Oracle") { const buildFile = JSON.parse(fs.readFileSync(path.join(ARTIFACTS_DIR, contractName + ".json"), { encoding: "utf8" })); - const contract = new web3.eth.Contract(abi); - const options = { data: bin, arguments: contractArgs }; + + const contract = new web3.eth.Contract(buildFile.abi); + const options = { data: buildFile.bytecode, arguments: contractArgs }; const transaction = contract.deploy(options); const receipt = await send(web3, account, gasPrice, transaction); const args = transaction.encodeABI().slice(options.data.length); console.log(`${contractId} deployed at ${receipt.contractAddress}`); - setConfig({ [contractId]: { name: contractName, addr: receipt.contractAddress, args: args } }); + + let configName = contractId; + let converterIndex = 0; + //TODO: IMPORTANT - FIGURE OUT HOW TO GET CONVERTER INDEX! IT WON'T WORK WITH MUlTIPLE CONVERTERS! + //TODO: move oracle to "converters" + //if (contractId === "Oracle") configName = `${contractId}${converterIndex++}`; + if (contractId === "Oracle") configName = `${contractId}`; + setConfig({ [configName]: { name: contractName, addr: receipt.contractAddress, args: args } }); + return deployed(web3, contractName, receipt.contractAddress); } return deployed(web3, contractName, getConfig()[contractId].addr); }; @@ -152,19 +155,35 @@ const percentageToPPM = (value) => { return decimalToInteger(value.replace("%", ""), 4); }; -const web3Func = (func, ...args) => func(web3, account, gasPrice, ...args); -const addresses = { ETH: Web3.utils.toChecksumAddress("0x".padEnd(42, "e")) }; -const tokenDecimals = { ETH: 18 }; - const addConverter = async (tokenOracleName, oracleMockName, oracleMockValue, oracleMockHas) => { - await initialiseWeb3(); + const web3 = new Web3(NODE_ADDRESS); + + const gasPrice = await getGasPrice(web3); + const account = web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY); + console.log("account: ", account); + const web3Func = (func, ...args) => func(web3, account, gasPrice, ...args); + const addresses = { ETH: Web3.utils.toChecksumAddress("0x".padEnd(42, "e")) }; + const tokenDecimals = { ETH: 18 }; + + let phase = 0; if (getConfig().phase === undefined) { setConfig({ phase }); } - const converterRegistry = await deployed(web3, "ConverterRegistry", getConfig().converterRegistry.addr); - const oracleWhitelist = await deployed(web3, "Whitelist", getConfig().oracleWhitelist.addr); + const execute = async (transaction, ...args) => { + if (getConfig().phase === phase++) { + await web3Func(send, transaction, ...args); + console.log(`phase ${phase} executed`); + setConfig({ phase }); + } + }; + + const converterRegistry = await deployed(web3, "ConverterRegistry", getData().converterRegistry.addr); + const oracleWhitelist = await deployed(web3, "Whitelist", getData().oracleWhitelist.addr); + + let multiSigWallet; + if (getData().multiSigWallet.addr !== "") multiSigWallet = deployed(web3, getData().multiSigWallet.name, getData().multiSigWallet.addr); //this block is just relevant for v2 pools let underlyingOracleAddress = []; @@ -232,7 +251,6 @@ const addConverter = async (tokenOracleName, oracleMockName, oracleMockValue, or await execute(converterRegistry.methods.newConverter(type, name, symbol, decimals, "1000000", tokens, weights)); await execute(converterRegistry.methods.setupConverter(type, tokens, weights, newConverter)); - const oracle = await web3Func(deploy, "oracle", "Oracle", [newConverter]); console.log("New Converter is ", newConverter); setConfig({ [`newLiquidityPoolV${type}Converter`]: { name: `LiquidityPoolV${type}Converter`, addr: newConverter, args: "" } }); @@ -241,7 +259,8 @@ const addConverter = async (tokenOracleName, oracleMockName, oracleMockValue, or const anchor = deployed(web3, "IConverterAnchor", (await converterRegistry.methods.getAnchors().call()).slice(-1)[0]); - console.log("Anchor Taken: ", anchor._address) + // TODO: Remove next line, just here for checking which address is received from anchor. The last address shown from above anchor list should be shown. + //console.log("Anchor Taken: ", anchor); const converterBase = deployed(web3, "ConverterBase", newConverter); @@ -252,11 +271,24 @@ const addConverter = async (tokenOracleName, oracleMockName, oracleMockValue, or console.log("Done with conversion fee"); if (type === 1) { + console.log("Deploying Oracle"); + const oracle = await web3Func(deploy, "Oracle", "Oracle", [converterBase._address, getConfig().btcAddress]); + + console.log("Setting k", getConfig().k); + await execute(oracle.methods.setK(getConfig().k)); + + console.log("Setting oracle in converter", oracle._address); const liquidityV1PoolConverter = deployed(web3, "LiquidityPoolV1Converter", converterBase._address); await execute(liquidityV1PoolConverter.methods.setOracle(oracle._address)); console.log("Done with adding oracle"); - } + if (multiSigWallet !== undefined) { + console.log("Updating oracle ownership"); + await execute(oracle.methods.transferOwnership(multiSigWallet._address)); + } else { + console.log("Oracle owner is account: ", await oracle.methods.owner().call()); + } + } //adding the liquidity and thereby seeting the price if (type !== 0 && amounts.every((amount) => amount > 0)) { for (let i = 0; i < converter.reserves.length; i++) { @@ -273,7 +305,7 @@ const addConverter = async (tokenOracleName, oracleMockName, oracleMockValue, or const oracleName = reserve.symbol === "RBTC" ? "MocBTCToUSDOracle" : tokenOracleName; //mocMedianizerMockUSDtoBTC is actually returning BTCtoUSD const mocOracleArgs = - oracleName === "MocBTCToUSDOracle" ? [getConfig().mocMedianizerMockUSDtoBTC.addr] : [underlyingOracleAddress]; + oracleName === "MocBTCToUSDOracle" ? [getData().mocMedianizerMockUSDtoBTC.addr] : [underlyingOracleAddress]; const mocPriceOracle = await web3Func( deploy, "mocPriceOracle" + converter.symbol + reserve.symbol, @@ -311,6 +343,7 @@ const addConverter = async (tokenOracleName, oracleMockName, oracleMockValue, or addresses[converter.symbol] = anchor._address; } + //TODO: add transfer ownership of converter to multisig console.log("All done"); if (web3.currentProvider.constructor.name === "WebsocketProvider") { diff --git a/solidity/utils/addConverterNoOracle.js b/solidity/utils/addConverterNoOracle.js new file mode 100644 index 0000000..248d4f4 --- /dev/null +++ b/solidity/utils/addConverterNoOracle.js @@ -0,0 +1,336 @@ +// This script is designed for deploying v1 converter for token pairs +// where none of them is wRBTC +// TODO: add to docs + +const fs = require("fs"); +const path = require("path"); +const Web3 = require("web3"); + +const TOKEN_NAME = process.argv[2]; +const TOKEN_CONFIG_FILENAME = process.argv[3]; +const DATA_FILENAME = process.argv[4]; +const NODE_ADDRESS = process.argv[5]; +const PRIVATE_KEY = process.argv[6]; + +const ARTIFACTS_DIR = path.resolve(__dirname, "../build/contracts"); + +const MIN_GAS_LIMIT = 100000; + +String.prototype.replaceAll = function (exp, newStr) { + return this.replace(new RegExp(exp, "gm"), newStr); +}; + +String.prototype.format = function (args) { + var result = this; + if (arguments.length < 1) { + return result; + } + + var data = arguments; + if (arguments.length == 1 && typeof args == "object") { + data = args; + } + for (var key in data) { + var value = data[key]; + if (undefined != value) { + result = result.replaceAll("\\{" + key + "\\}", value); + } + } + return result; +}; + +const getConfig = () => { + //return JSON.parse(fs.readFileSync(path.join(__dirname, "add{token}.json".format({ token: TOKEN_NAME })), { encoding: "utf8" })); + return JSON.parse(fs.readFileSync(path.join(__dirname, TOKEN_CONFIG_FILENAME), { encoding: "utf8" })); +}; + +const getData = () => { + //TODO read the config according to the network; temporarily moved to input params + //return JSON.parse(fs.readFileSync("./config_rsk_testnet.json", { encoding: "utf8" })); + return JSON.parse(fs.readFileSync(DATA_FILENAME, { encoding: "utf8" })); +}; + +const setConfig = (record) => { + fs.writeFileSync(path.join(__dirname, TOKEN_CONFIG_FILENAME), JSON.stringify({ ...getConfig(), ...record }, null, 4)); +}; + +const scan = async (message) => { + process.stdout.write(message); + return await new Promise((resolve, reject) => { + process.stdin.resume(); + process.stdin.once("data", (data) => { + process.stdin.pause(); + resolve(data.toString().trim()); + }); + }); +}; + +const getGasPrice = async (web3) => { + while (true) { + const nodeGasPrice = await web3.eth.getGasPrice(); + const userGasPrice = await scan(`Enter gas-price or leave empty to use ${nodeGasPrice}: `); + if (/^\d+$/.test(userGasPrice)) { + return userGasPrice; + } + if (userGasPrice === "") { + return nodeGasPrice; + } + console.log("Illegal gas-price"); + } +}; + +const getTransactionReceipt = async (web3) => { + while (true) { + const hash = await scan("Enter transaction-hash or leave empty to retry: "); + if (/^0x([0-9A-Fa-f]{64})$/.test(hash)) { + const receipt = await web3.eth.getTransactionReceipt(hash); + if (receipt) { + return receipt; + } + console.log("Invalid transaction-hash"); + } else if (hash) { + console.log("Illegal transaction-hash"); + } else { + return null; + } + } +}; + +const send = async (web3, account, gasPrice, transaction, value = 0) => { + while (true) { + try { + const gasEstimate = await transaction.estimateGas({ from: account.address, value: value }); + console.log("gasEstimate: " + gasEstimate, " - value - ", value); + const tx = { + to: transaction._parent._address, + data: transaction.encodeABI(), + gas: Math.max(await transaction.estimateGas({ from: account.address, value: value }), MIN_GAS_LIMIT), + gasPrice: gasPrice || (await getGasPrice(web3)), + chainId: await web3.eth.net.getId(), + value: value, + }; + const signed = await web3.eth.accounts.signTransaction(tx, account.privateKey); + const receipt = await web3.eth.sendSignedTransaction(signed.rawTransaction); + return receipt; + } catch (error) { + console.log(error.message); + const receipt = await getTransactionReceipt(web3); + if (receipt) { + return receipt; + } + } + } +}; + +const deploy = async (web3, account, gasPrice, contractId, contractName, contractArgs) => { + if (getConfig()[contractId] === undefined || contractId === "Oracle") { + const buildFile = JSON.parse(fs.readFileSync(path.join(ARTIFACTS_DIR, contractName + ".json"), { encoding: "utf8" })); + + const contract = new web3.eth.Contract(buildFile.abi); + const options = { data: buildFile.bytecode, arguments: contractArgs }; + const transaction = contract.deploy(options); + const receipt = await send(web3, account, gasPrice, transaction); + const args = transaction.encodeABI().slice(options.data.length); + console.log(`${contractId} deployed at ${receipt.contractAddress}`); + setConfig({ [contractId]: { name: contractName, addr: receipt.contractAddress, args: args } }); + } + return deployed(web3, contractName, getConfig()[contractId].addr); +}; + +const deployed = (web3, contractName, contractAddr) => { + const buildFile = JSON.parse(fs.readFileSync(path.join(ARTIFACTS_DIR, contractName + ".json"), { encoding: "utf8" })); + return new web3.eth.Contract(buildFile.abi, contractAddr); +}; + +const decimalToInteger = (value, decimals) => { + const parts = [...value.split("."), ""]; + return parts[0] + parts[1].padEnd(decimals, "0"); +}; + +const percentageToPPM = (value) => { + return decimalToInteger(value.replace("%", ""), 4); +}; + +const addConverter = async (tokenOracleName, oracleMockName, oracleMockValue, oracleMockHas) => { + const web3 = new Web3(NODE_ADDRESS); + + const gasPrice = await getGasPrice(web3); + const account = web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY); + console.log("account: ", account); + const web3Func = (func, ...args) => func(web3, account, gasPrice, ...args); + + const addresses = { ETH: Web3.utils.toChecksumAddress("0x".padEnd(42, "e")) }; + const tokenDecimals = { ETH: 18 }; + + let phase = 0; + if (getConfig().phase === undefined) { + setConfig({ phase }); + } + + const execute = async (transaction, ...args) => { + if (getConfig().phase === phase++) { + await web3Func(send, transaction, ...args); + console.log(`phase ${phase} executed`); + setConfig({ phase }); + } + }; + + const converterRegistry = await deployed(web3, "ConverterRegistry", getData().converterRegistry.addr); + const oracleWhitelist = await deployed(web3, "Whitelist", getData().oracleWhitelist.addr); + + let multiSigWallet; + if (getData().multiSigWallet.addr !== "") multiSigWallet = deployed(web3, getData().multiSigWallet.name, getData().multiSigWallet.addr); + + //this block is just relevant for v2 pools + let underlyingOracleAddress = []; + if (oracleMockName != undefined) { + //if underlying address is defined, use it + if (getConfig().underlyingOracleAddress !== undefined) { + underlyingOracleAddress = getConfig().underlyingOracleAddress; + } + //else deploy a mockup + else { + console.log("deploying a mockup"); + const oracleMock = await web3Func(deploy, oracleMockName, oracleMockName, []); + underlyingOracleAddress = oracleMock._address; + if (oracleMockName != undefined) { + await execute(oracleMock.methods.setValue(oracleMockValue)); + } + if (oracleMockHas != undefined) { + await execute(oracleMock.methods.setHas(oracleMockHas)); + } + const tokenOracle = await web3Func(deploy, tokenOracleName, tokenOracleName, [underlyingOracleAddress]); + } + } + + //read reserve data from the config or deploy new tokens if not defined + for (const reserve of getConfig().reserves) { + if (reserve.address) { + addresses[reserve.symbol] = reserve.address; + tokenDecimals[reserve.symbol] = reserve.decimals; + setConfig({ [reserve.symbol]: { name: reserve.symbol, addr: reserve.address } }); + } else { + t = await web3Func(deploy, reserve.symbol, "ERC20Token", [ + reserve.symbol, + reserve.symbol, + reserve.decimals, + decimalToInteger("10000", reserve.decimals), + ]); + tokenDecimals[reserve.symbol] = reserve.decimals; + addresses[reserve.symbol] = t._address; + } + } + + for (const converter of getConfig().converters) { + const type = converter.type; + const name = converter.symbol + (type == 0 ? " Liquid Token" : " Liquidity Pool"); + const symbol = converter.symbol; + const decimals = converter.decimals; + const fee = percentageToPPM(converter.fee); + + const tokens = converter.reserves.map((reserve) => addresses[reserve.symbol]); + const weights = converter.reserves.map((reserve) => percentageToPPM(reserve.weight)); + const amounts = converter.reserves.map((reserve) => decimalToInteger(reserve.balance, tokenDecimals[reserve.symbol])); + const value = 0; // amounts[converter.reserves.findIndex(reserve => reserve.symbol === 'RBTC')]; + + console.log("Deploying converter for ", type, " - ", name, " with value ", value); + if (getConfig()["phase"] > 0) console.log(`Restarting from phase #${getConfig()["phase"]}`); + + let newConverter; + //if the script breaks during execution, run it again, it will resume from the point of failure automagically + if (getConfig()["phase"] < 2) { + newConverter = await converterRegistry.methods.newConverter(type, name, symbol, decimals, "1000000", tokens, weights).call(); + } else { + newConverter = getConfig()[`newLiquidityPoolV${type}Converter`].addr; + console.log("Using previously created converter ", newConverter); + } + + await execute(converterRegistry.methods.newConverter(type, name, symbol, decimals, "1000000", tokens, weights)); + await execute(converterRegistry.methods.setupConverter(type, tokens, weights, newConverter)); + console.log("New Converter is ", newConverter); + setConfig({ [`newLiquidityPoolV${type}Converter`]: { name: `LiquidityPoolV${type}Converter`, addr: newConverter, args: "" } }); + + console.log("Calling anchors"); + console.log(await converterRegistry.methods.getAnchors().call()); + + const anchor = deployed(web3, "IConverterAnchor", (await converterRegistry.methods.getAnchors().call()).slice(-1)[0]); + + // TODO: Remove next line, just here for checking which address is received from anchor. The last address shown from above anchor list should be shown. + console.log("Anchor Taken: ", anchor._address) + + const converterBase = deployed(web3, "ConverterBase", newConverter); + + console.log("Now executing the settings on " + converterBase._address); + await execute(converterBase.methods.acceptOwnership()); + console.log("Done with ownership acceptance"); + await execute(converterBase.methods.setConversionFee(fee)); + console.log("Done with conversion fee"); + + //adding the liquidity and thereby seeting the price + if (type !== 0 && amounts.every((amount) => amount > 0)) { + for (let i = 0; i < converter.reserves.length; i++) { + const reserve = converter.reserves[i]; + + console.log("Approving amount for ERC20Token: " + amounts[i]); + await execute(deployed(web3, "ERC20Token", tokens[i]).methods.approve(converterBase._address, amounts[i])); + let availableBalance = await deployed(web3, "ERC20Token", tokens[i]).methods.balanceOf(account.address).call(); + console.log("available balance: "); + console.log(availableBalance); + + if (type == 2) { + if (!reserve.oracle) { + const oracleName = reserve.symbol === "RBTC" ? "MocBTCToUSDOracle" : tokenOracleName; + //mocMedianizerMockUSDtoBTC is actually returning BTCtoUSD + const mocOracleArgs = + oracleName === "MocBTCToUSDOracle" ? [getData().mocMedianizerMockUSDtoBTC.addr] : [underlyingOracleAddress]; + const mocPriceOracle = await web3Func( + deploy, + "mocPriceOracle" + converter.symbol + reserve.symbol, + oracleName, + mocOracleArgs + ); + reserve.oracle = mocPriceOracle._address; + } + console.log("reserve.oracle", reserve.oracle); + await execute(oracleWhitelist.methods.addAddress(reserve.oracle)); + } + } + + if (type == 1) { + console.log("adding liquidity"); + console.log("tokens:"); + console.log(tokens); + console.log("amounts:"); + console.log(amounts); + console.log("converterBase._address:"); + console.log(converterBase._address); + await execute(deployed(web3, "LiquidityPoolV1Converter", converterBase._address).methods.addLiquidity(tokens, amounts, 1), value); + } else if (type == 2) { + const deployedConverter = deployed(web3, "LiquidityPoolV2Converter", converterBase._address); + await execute(deployedConverter.methods.activate(tokens[0], converter.reserves[0].oracle, converter.reserves[1].oracle)); + + for (let i = 0; i < converter.reserves.length; i++) { + console.log("Adding liquidity for LiquidityPoolV2Converter. ", "For token: ", tokens[i], " with amount ", amounts[i]); + await execute(deployedConverter.methods.addLiquidity(tokens[i], amounts[i], 1), value); + console.log("Liquidity added"); + } + } + } + + addresses[converter.symbol] = anchor._address; + } + + console.log("All done"); + + if (web3.currentProvider.constructor.name === "WebsocketProvider") { + web3.currentProvider.connection.close(); + } +}; + +if (TOKEN_NAME === "BPro") { + addConverter("BProOracle", "MoCStateMock", "20000000000000000000000"); +} else if (TOKEN_NAME === "USDT") { + addConverter("MocBTCToBTCOracle"); +} else { + addConverter(); +} diff --git a/solidity/utils/addFish_mainnet.json b/solidity/utils/addFish_mainnet.json new file mode 100644 index 0000000..ecffd40 --- /dev/null +++ b/solidity/utils/addFish_mainnet.json @@ -0,0 +1,55 @@ +{ + "reserves": [ + { + "symbol": "(WR)BTC", + "decimals": 18, + "address": "0x542fDA317318eBF1d3DEAf76E0b632741A7e677d" + }, + { + "symbol": "FISH", + "decimals": 18, + "address": "0x055A902303746382FBB7D18f6aE0df56eFDc5213" + } + ], + "converters": [ + { + "type": 1, + "symbol": "(WR)BTC/FISH", + "decimals": 18, + "fee": "0.3%", + "reserves": [ + { + "symbol": "(WR)BTC", + "weight": "50%", + "balance": "0" + }, + { + "symbol": "FISH", + "weight": "50%", + "balance": "0" + } + ] + } + ], + "k": 6779, + "btcAddress": "0x542fDA317318eBF1d3DEAf76E0b632741A7e677d", + "phase": 7, + "(WR)BTC": { + "name": "(WR)BTC", + "addr": "0x542fDA317318eBF1d3DEAf76E0b632741A7e677d" + }, + "FISH": { + "name": "FISH", + "addr": "0x055A902303746382FBB7D18f6aE0df56eFDc5213" + }, + "newLiquidityPoolV1Converter": { + "name": "LiquidityPoolV1Converter", + "addr": "0xe731DA93034D769c2045B1ee137D42E1Aa23C18e", + "args": "" + }, + "Oracle": { + "name": "Oracle", + "addr": "0x6C58148993bB918347D6d4e0E67EE47903776ab7", + "args": "000000000000000000000000e731da93034d769c2045b1ee137d42e1aa23c18e000000000000000000000000542fda317318ebf1d3deaf76e0b632741a7e677d" + } +} \ No newline at end of file diff --git a/solidity/utils/addFish_testnet.json b/solidity/utils/addFish_testnet.json new file mode 100644 index 0000000..e8a2368 --- /dev/null +++ b/solidity/utils/addFish_testnet.json @@ -0,0 +1,55 @@ +{ + "reserves": [ + { + "symbol": "(WR)BTC", + "decimals": 18, + "address": "0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab" + }, + { + "symbol": "FISH", + "decimals": 18, + "address": "0xaa7038D80521351F243168FefE0352194e3f83C3" + } + ], + "converters": [ + { + "type": 1, + "symbol": "(WR)BTC/FISH", + "decimals": 18, + "fee": "0.3%", + "reserves": [ + { + "symbol": "(WR)BTC", + "weight": "50%", + "balance": "0" + }, + { + "symbol": "FISH", + "weight": "50%", + "balance": "0" + } + ] + } + ], + "k": 6779, + "btcAddress": "0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab", + "phase": 6, + "(WR)BTC": { + "name": "(WR)BTC", + "addr": "0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab" + }, + "FISH": { + "name": "FISH", + "addr": "0xaa7038D80521351F243168FefE0352194e3f83C3" + }, + "newLiquidityPoolV1Converter": { + "name": "LiquidityPoolV1Converter", + "addr": "0x179caA42B5024ec1C3D8513A262fC9986F565295", + "args": "" + }, + "Oracle": { + "name": "Oracle", + "addr": "0x498E4D1d39968b0BB5DECD52D71055529150ba74", + "args": "000000000000000000000000179caa42b5024ec1c3d8513a262fc9986f56529500000000000000000000000069fe5cec81d5ef92600c1a0db1f11986ab3758ab" + } +} \ No newline at end of file diff --git a/solidity/utils/command.txt b/solidity/utils/command.txt index a3ca65e..561f43c 100644 --- a/solidity/utils/command.txt +++ b/solidity/utils/command.txt @@ -12,4 +12,4 @@ testnet: node addConverter.js ETH addETHs_testnet.json data_testnet.json https://public-node.testnet.rsk.co mainnet: -node addConverter.js ETH addETHs_mainnet.json data_mainnet.json https://mainnet.sovryn.app/rpc +node addConverter.js ETH addETHs_mainnet.json data_mainnet.json https://mainnet2.sovryn.app/rpc diff --git a/solidity/utils/data_mainnet.json b/solidity/utils/data_mainnet.json index e68c485..69edfa3 100644 --- a/solidity/utils/data_mainnet.json +++ b/solidity/utils/data_mainnet.json @@ -112,5 +112,10 @@ "name": "MocBTCToBTCOracle", "addr": "0xf5DF3b2AE0c4E2c8912E177f6BD8ca6d479397A2", "args": "" + }, + "multiSigWallet": { + "name": "MultiSigWallet", + "addr": "0x924f5ad34698Fd20c90Fe5D5A8A0abd3b42dc711", + "args": "" } -} +} \ No newline at end of file diff --git a/solidity/utils/data_testnet.json b/solidity/utils/data_testnet.json index dfdf3e1..7efb5fa 100644 --- a/solidity/utils/data_testnet.json +++ b/solidity/utils/data_testnet.json @@ -111,5 +111,10 @@ "name": "MocBTCToBTCOracle", "addr": "0xf37963592635c020E0AD3313bA39C9BA627a3FB5", "args": "" + }, + "multiSigWallet": { + "name": "MultiSigWallet", + "addr": "", + "args": "" } } \ No newline at end of file diff --git a/solidity/utils/upgrade_config.json b/solidity/utils/upgrade_config.json index 864d1b8..20ec4fe 100644 --- a/solidity/utils/upgrade_config.json +++ b/solidity/utils/upgrade_config.json @@ -1,45 +1,45 @@ { - "privateKey": "0x", - "nodeURL": "https://mainnet.sovryn.app/rpc", - "type": 1, - "btcAddress": "0x542fDA317318eBF1d3DEAf76E0b632741A7e677d", - "k": 6779, - "block": 3550895, - "converterFactory": { - "name": "ConverterFactory", - "addr": "0xcF46f24423B8da97E2c06B41df28163D55e80935", - "args": "" - }, - "converterUpgrader": { - "name": "ConverterUpgrader", - "addr": "0x40346f7Ce14B3a10bAB22F6a0A444902CF0Bc598", - "args": "" - }, - "converterRegistry": { - "name": "ConverterRegistry", - "addr": "0x31A0F8400c75d52FdB413372233F28E3bdFB1c06", - "args": "" - }, - "multiSigWallet": { - "name": "MultiSigWallet", - "addr": "0x924f5ad34698Fd20c90Fe5D5A8A0abd3b42dc711", - "args": "" - }, - "converterContract": { - "name": "LiquidityPoolV1Converter", - "addr": [ - "0x9996E5F902d2d804E9eD0DdB1B628D1EBf6Bb6fE" - ], - "args": "" - }, - "liquidityPoolV1ConverterFactory": { - "name": "LiquidityPoolV1ConverterFactory", - "addr": "0x1E7428B34Af8FA6Dc40d7A38769efA948E065669", - "args": "" - }, - "Oracle0": { - "name": "Oracle", - "addr": "0x345f2eC520431db542e33eD31a8f687E8eC705BB", - "args": "00000000000000000000000034031d1cd14e2c80b0268b47eff49643375afaeb000000000000000000000000542fda317318ebf1d3deaf76e0b632741a7e677d" - } + "privateKey": "0x", + "nodeURL": "https://mainnet.sovryn.app/rpc", + "type": 1, + "btcAddress": "0x542fDA317318eBF1d3DEAf76E0b632741A7e677d", + "k": 6779, + "block": 3550895, + "converterFactory": { + "name": "ConverterFactory", + "addr": "0xcF46f24423B8da97E2c06B41df28163D55e80935", + "args": "" + }, + "converterUpgrader": { + "name": "ConverterUpgrader", + "addr": "0x40346f7Ce14B3a10bAB22F6a0A444902CF0Bc598", + "args": "" + }, + "converterRegistry": { + "name": "ConverterRegistry", + "addr": "0x31A0F8400c75d52FdB413372233F28E3bdFB1c06", + "args": "" + }, + "multiSigWallet": { + "name": "MultiSigWallet", + "addr": "0x924f5ad34698Fd20c90Fe5D5A8A0abd3b42dc711", + "args": "" + }, + "converterContract": { + "name": "LiquidityPoolV1Converter", + "addr": [ + "0x9996E5F902d2d804E9eD0DdB1B628D1EBf6Bb6fE" + ], + "args": "" + }, + "liquidityPoolV1ConverterFactory": { + "name": "LiquidityPoolV1ConverterFactory", + "addr": "0x1E7428B34Af8FA6Dc40d7A38769efA948E065669", + "args": "" + }, + "Oracle0": { + "name": "Oracle", + "addr": "0x345f2eC520431db542e33eD31a8f687E8eC705BB", + "args": "00000000000000000000000034031d1cd14e2c80b0268b47eff49643375afaeb000000000000000000000000542fda317318ebf1d3deaf76e0b632741a7e677d" + } } \ No newline at end of file