diff --git a/lib/check-allowance.js b/lib/check-allowance.js deleted file mode 100644 index eed255a..0000000 --- a/lib/check-allowance.js +++ /dev/null @@ -1,29 +0,0 @@ -const { BigNumber } = require('ethers') -const { logic } = require('ptokens-utils') -const { getTokenContract } = require('./get-token-contract') - -const TIMEOUT_TIME = 3000 - -const checkAllowanceIsSufficient = ( - _amount, - _spenderAddress, - _ownerAddress, - _tokenAddress, - _signer, -) => - console.info(`✔ Checking spender ${_spenderAddress} has sufficient allowance for token ${_tokenAddress}...`) || - logic.racePromise( - TIMEOUT_TIME, - getTokenContract(_tokenAddress, _signer).allowance, - [ _ownerAddress, _spenderAddress ], - ) - .then(_allowance => { - if (_allowance.lt(BigNumber.from(_amount))) { - return Promise.reject(new Error(`Insufficient allowance to peg in! Got ${_allowance}, need ${_amount}!`)) - } else { - console.info(`✔ Allowance of ${_allowance} is sufficient!`) - return - } - }) - -module.exports = { checkAllowanceIsSufficient } diff --git a/lib/check-token-balance.js b/lib/check-token-balance.js new file mode 100644 index 0000000..7150c3e --- /dev/null +++ b/lib/check-token-balance.js @@ -0,0 +1,24 @@ +const { BigNumber } = require('ethers') +const { logic } = require('ptokens-utils') +const { getTokenContract } = require('./get-token-contract') +const { CONTRACT_CALL_TIME_OUT_TIME } = require('./constants') + +const checkTokenBalanceIsSufficient = (_amount, _tokenOwnerAddress, _tokenAddress, _signer) => + console.info(`✔ Checking spender ${_tokenOwnerAddress} has sufficient balance of token ${_tokenAddress}...`) || + logic.racePromise( + CONTRACT_CALL_TIME_OUT_TIME, + getTokenContract(_tokenAddress, _signer).balanceOf, + [ _tokenOwnerAddress ], + ) + .then(_tokenBalance => { + if (_tokenBalance.lt(BigNumber.from(_amount))) { + return Promise.reject( + new Error(`Insufficient token balance to peg in! Got ${_tokenBalance}, need ${_amount}!`) + ) + } else { + console.info(`✔ Token balance of ${_tokenBalance} is sufficient!`) + return + } + }) + +module.exports = { checkTokenBalanceIsSufficient } diff --git a/lib/check-token-is-supported.js b/lib/check-token-is-supported.js new file mode 100644 index 0000000..f31d609 --- /dev/null +++ b/lib/check-token-is-supported.js @@ -0,0 +1,34 @@ +const { logic } = require('ptokens-utils') +const { getEthersContract } = require('./utils') +const { CONTRACT_CALL_TIME_OUT_TIME } = require('./constants') + +const VAULT_ABI_FRAGMENT = [{ + 'type': 'function', + 'stateMutability': 'view', + 'name': 'isTokenSupported', + 'outputs': [{ 'internalType': 'bool', 'name': '', 'type': 'bool' }], + 'inputs': [{ 'internalType': 'address', 'name': '_token', 'type': 'address' }], +}] + +const getVaultContract = (_address, _provider) => + getEthersContract(_address, VAULT_ABI_FRAGMENT, _provider) + +const checkTokenIsSupportedInVault = (_vaultAddress, _tokenAddress, _provider) => + console.info(`✔ Checking token @ ${_tokenAddress} is supported in vault @ ${_vaultAddress}...`) || + logic.racePromise( + CONTRACT_CALL_TIME_OUT_TIME, + getVaultContract(_vaultAddress, _provider).isTokenSupported, + [ _tokenAddress ], + ) + .then(_tokenIsSupported => { + if (_tokenIsSupported) { + console.info('✔ Token is supported in vault contract!') + return + } else { + return Promise.reject( + new Error(`Token @ ${_tokenAddress} is NOT supported in vault contract @ ${_vaultAddress}!`) + ) + } + }) + +module.exports = { checkTokenIsSupportedInVault } diff --git a/lib/constants.js b/lib/constants.js index 173308a..d462940 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,5 +1,6 @@ module.exports = { ENDPOINT_ENV_VAR_KEY: 'ENDPOINT', + CONTRACT_CALL_TIME_OUT_TIME: 3000, ETHERSCAN_ENV_VAR_KEY: 'ETHERSCAN_API_KEY', HARDHAT_CONFIG_FILE_NAME: 'hardhat.config.js', FLATTENED_CONTRACT_FILE_NAME: 'flattened.sol', diff --git a/lib/get-token-contract.js b/lib/get-token-contract.js index a5a30b5..02f065c 100644 --- a/lib/get-token-contract.js +++ b/lib/get-token-contract.js @@ -1,20 +1,37 @@ -/* eslint-disable-next-line no-shadow */ -const ethers = require('ethers') +const { getEthersContract } = require('./utils') -const TOKEN_ABI = [{ - 'inputs': [ - { 'internalType': 'address', 'name': 'holder', 'type': 'address' }, - { 'internalType': 'address', 'name': 'spender', 'type': 'address' } - ], - 'name': 'allowance', - 'outputs': [ { 'internalType': 'uint256', 'name': '', 'type': 'uint256' } ], - 'stateMutability': 'view', - 'type': 'function' -}] +const TOKEN_ABI = [ + { + 'inputs': [ + { 'internalType': 'address', 'name': 'holder', 'type': 'address' }, + { 'internalType': 'address', 'name': 'spender', 'type': 'address' } + ], + 'name': 'allowance', + 'outputs': [ { 'internalType': 'uint256', 'name': '', 'type': 'uint256' } ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ { 'internalType': 'address', 'name': 'tokenHolder', 'type': 'address' } ], + 'name': 'balanceOf', + 'outputs': [ { 'internalType': 'uint256', 'name': '', 'type': 'uint256' } ], + 'stateMutability': 'view', + 'type': 'function' + }, + { + 'inputs': [ + { 'internalType': 'address', 'name': 'spender', 'type': 'address' }, + { 'internalType': 'uint256', 'name': 'value', 'type': 'uint256' } + ], + 'name': 'approve', + 'outputs': [ { 'internalType': 'bool', 'name': '', 'type': 'bool' } ], + 'stateMutability': 'nonpayable', + 'type': 'function' + }, +] -const getTokenContract = (_address, _signer) => { - console.info(`✔ Getting token contract @ '${_address}'...`) - return new ethers.Contract(_address, TOKEN_ABI, _signer) -} +const getTokenContract = (_address, _signer) => + console.info(`✔ Getting token contract @ '${_address}'...`) || + getEthersContract(_address, TOKEN_ABI, _signer) module.exports = { getTokenContract } diff --git a/lib/get-vault-contract.js b/lib/get-vault-contract.js index 79d7946..df036c4 100644 --- a/lib/get-vault-contract.js +++ b/lib/get-vault-contract.js @@ -1,18 +1,17 @@ -/* eslint-disable-next-line no-shadow */ -const ethers = require('ethers') const { curry } = require('ramda') +const { getEthersContract } = require('./utils') const { getProvider } = require('./get-provider') const { checkEndpoint } = require('./check-endpoint') -const { getVaultAbi } = require('./get-contract-artifacts') const { ENDPOINT_ENV_VAR_KEY } = require('./constants') const { getEthersWallet } = require('./get-ethers-wallet') +const { getVaultAbi } = require('./get-contract-artifacts') const { getEnvConfiguration } = require('./get-env-configuration') const { getEnvironmentVariable } = require('./get-environment-variable') -const getEthersContract = curry((_address, _abi, _signer) => { - console.info(`✔ Getting contract @ '${_address}'...`) - return Promise.resolve(new ethers.Contract(_address, _abi, _signer)) -}) +const getVaultEthersContract = curry((_address, _signer) => + console.info('✔ Getting vault contract...') || + getVaultAbi().then(_abi => getEthersContract(_address, _abi, _signer)) +) const getVaultContract = _deployedContractAddress => console.info(`✔ Getting pToken contract @ '${_deployedContractAddress}'...`) || @@ -20,8 +19,8 @@ const getVaultContract = _deployedContractAddress => .then(() => getEnvironmentVariable(ENDPOINT_ENV_VAR_KEY)) .then(getProvider) .then(checkEndpoint) - .then(_endpoint => Promise.all([ getEthersWallet(_endpoint), getVaultAbi() ])) - .then(([ _wallet, _abi ]) => getEthersContract(_deployedContractAddress, _abi, _wallet)) - .then(_contract => console.info('✔ Contract retrieved!') || _contract) + .then(getEthersWallet) + .then(getVaultEthersContract(_deployedContractAddress)) + .then(_contract => console.info('✔ Vault contract retrieved!') || _contract) module.exports = { getVaultContract } diff --git a/lib/maybe-approve-allowance.js b/lib/maybe-approve-allowance.js new file mode 100644 index 0000000..1cee77d --- /dev/null +++ b/lib/maybe-approve-allowance.js @@ -0,0 +1,33 @@ +const { BigNumber } = require('ethers') +const { logic } = require('ptokens-utils') +const { getTokenContract } = require('./get-token-contract') +const { CONTRACT_CALL_TIME_OUT_TIME } = require('./constants') + +const approveAllowance = (_amount, _vaultAddress, _tokenAddress, _signer) => + console.info(`✔ Approving vault to spend ${_amount} tokens, please wait for mining...`) || + getTokenContract(_tokenAddress, _signer) + .approve(_vaultAddress, _amount) + .then(_tx => _tx.wait()) + .then(_ => console.info('✔ Approval succeeded! Continuing with peg in...')) + +const maybeApproveAllowance = (_amount, _vaultAddress, _ownerAddress, _tokenAddress, _signer) => + console.info(`✔ Checking spender ${_vaultAddress} has sufficient allowance for token ${_tokenAddress}...`) || + logic.racePromise( + CONTRACT_CALL_TIME_OUT_TIME, + getTokenContract(_tokenAddress, _signer).allowance, + [ _ownerAddress, _vaultAddress ], + ) + .then(_allowance => { + if (_allowance.lt(BigNumber.from(_amount))) { + // NOTE: At least one of the tokens we bridge has a special approval mechanism whereby if we + // want to change an allowance that is already > 0, we must first set it to 0 and then to our + // desired amount. This logic does NOT currently handle this case. + console.info('✘ Allowance is not sufficient!') + return approveAllowance(_amount, _vaultAddress, _tokenAddress, _signer) + } else { + console.info(`✔ Allowance of ${_allowance} is already sufficient!`) + return + } + }) + +module.exports = { maybeApproveAllowance } diff --git a/lib/peg-in.js b/lib/peg-in.js index 97aba67..11dae63 100644 --- a/lib/peg-in.js +++ b/lib/peg-in.js @@ -1,9 +1,11 @@ const { getVaultContract } = require('./get-vault-contract') -const { checkAllowanceIsSufficient } = require('./check-allowance') +const { maybeApproveAllowance } = require('./maybe-approve-allowance') const { callFxnInContractAndAwaitReceipt } = require('./contract-utils') +const { checkTokenBalanceIsSufficient } = require('./check-token-balance') +const { checkTokenIsSupportedInVault } = require('./check-token-is-supported') const pegIn = ( - _deployedContractAddress, + _vaultAddress, _amount, _tokenAddress, _destinationAddress, @@ -11,18 +13,26 @@ const pegIn = ( _userData = '0x', ) => console.info('✔ Pegging in...') || - getVaultContract(_deployedContractAddress) - .then(_vaultContact => Promise.all([ _vaultContact, _vaultContact.signer.getAddress() ])) - .then(([ _vaultContract, _ownerAddress ]) => + getVaultContract(_vaultAddress) + .then(_vaultContract => Promise.all([ _vaultContract, _vaultContract.signer.getAddress() ])) + .then(([ _vaultContract, _tokenOwnerAddress ]) => Promise.all([ _vaultContract, - checkAllowanceIsSufficient( - _amount, - _deployedContractAddress, // NOTE: This is the "spender" address, the vault in this case! - _ownerAddress, - _tokenAddress, - _vaultContract.provider, - ) + _tokenOwnerAddress, + checkTokenIsSupportedInVault(_vaultAddress, _tokenAddress, _vaultContract.provider), + ]) + ) + .then(([ _vaultContract, _tokenOwnerAddress, ]) => + Promise.all([ + _vaultContract, + _tokenOwnerAddress, + checkTokenBalanceIsSufficient(_amount, _tokenOwnerAddress, _tokenAddress, _vaultContract.provider) + ]) + ) + .then(([ _vaultContract, _tokenOwnerAddress ]) => + Promise.all([ + _vaultContract, + maybeApproveAllowance(_amount, _vaultAddress, _tokenOwnerAddress, _tokenAddress, _vaultContract.signer) ]) ) .then(([ _vaultContract ]) => diff --git a/lib/utils.js b/lib/utils.js index 4451d7f..97635f4 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,7 @@ const { has, prop, + curry, } = require('ramda') /* eslint-disable-next-line no-shadow */ const ethers = require('ethers') @@ -46,11 +47,17 @@ const maybeHandleEtherscanTrimErrMsg = _err => )) : Promise.reject(_err) +const getEthersContract = curry((_address, _abi, _signer) => { + console.info(`✔ Getting contract @ '${_address}'...`) + return new ethers.Contract(_address, _abi, _signer) +}) + module.exports = { maybeHandleEtherscanTrimErrMsg, maybeStripHexPrefix, maybeAddHexPrefix, shortenEthAddress, + getEthersContract, checkEthAddress, getKeyFromObj, checkIsHex, diff --git a/package.json b/package.json index fa5188a..8b5eef5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ptokens-erc20-vault-smart-contract", - "version": "2.5.0", + "version": "2.6.0", "description": "The pToken ERC20 vault smart-contract & CLI", "main": "cli.js", "scripts": {