diff --git a/.alusd.env b/.alusd.env new file mode 100644 index 0000000..9e938d7 --- /dev/null +++ b/.alusd.env @@ -0,0 +1,22 @@ +# DEFAULT_GAS_PRICE= 50 GWEI by default + +TOKEN=0xBC6DA0FE9aD5f3b0d58160288917AA56653660E9 +ATOKEN=0x174aC37dD1db54516D00887E0E84697423Eefa9b +STABLE_DEBT_TOKEN=0x1D708aB94699717A4A8d725431d322D418A166E8 +VARIABLE_DEBT_TOKEN=0xE2B3b50CF9b36C8d066e5da994B97cae6a268482 +INTEREST_STRATEGY=0x0CFc9A0F66692179abCA661a610851bdf6fAE0F0 +LTV=0 +LIQUIDATION_THRESHOLD=0 +LIQUIDATION_BONUS=0 +RESERVE_FACTOR=2000 +DECIMALS=18 +ENABLE_BORROW=true +ENABLE_STABLE_BORROW=true +ENABLE_AS_COLLATERAL=false +CHAINLINK_ORACLE_PROXY=0xF55DB61d1e65718ac0d5A163B18CCA3645791265 + +TOKEN_HOLDER=0xB05136FAcE3F8c6f9e6CDb076a38Ee6D4910f6d4 +FORKING_BLOCK=14367337 + +# IPFS hash is a placeholder until PR is merged +IPFS_HASH=QmNfU4FMdQriJVQeqQTNxgY63iSJVh8yCJf8aFDkQDjaLQ \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index b872ef4..630c8f5 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -13,6 +13,7 @@ if (process.env.SKIP_LOAD !== 'true') { require('./tasks/list-new-asset.ts'); require('./tasks/list-rai.ts'); require('./tasks/list-bond.ts'); + require('./tasks/list-alusd.ts') } export const BUIDLEREVM_CHAIN_ID = 31337; diff --git a/package.json b/package.json index aae16b9..ea5b564 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ "test:bond": "MAINNET_FORK=true FORKING_BLOCK=12767300 hardhat test ./test/test-listing-bond.spec.ts", "test:xsushi": "MAINNET_FORK=true FORKING_BLOCK=11829845 hardhat test ./test/test-listing-xsushi.spec.ts", "test:rai": "MAINNET_FORK=true FORKING_BLOCK=12593370 hardhat test ./test/test-listing-rai.spec.ts", + "test:alusd": "MAINNET_FORK=true FORKING_BLOCK=14367337 hardhat test ./test/test-listing-alusd.spec.ts", "run-env": "npm i && tail -f /dev/null", "propose-new-asset:kovan": "hardhat create:proposal-new-asset --network kovan", "propose-new-asset:main": "hardhat create:proposal-new-asset --network main", "propose-new-asset:bond:main": "hardhat create:proposal-new-asset:bond --network main", - "propose-new-asset:rai:main": "hardhat create:proposal-new-asset:rai --network main" + "propose-new-asset:rai:main": "hardhat create:proposal-new-asset:rai --network main", + "propose-new-asset:alusd:main": "hardhat create:proposal-new-asset:alusd --network main" }, "keywords": [], "author": "dhadrien", diff --git a/tasks/list-alusd.ts b/tasks/list-alusd.ts new file mode 100644 index 0000000..96951bd --- /dev/null +++ b/tasks/list-alusd.ts @@ -0,0 +1,125 @@ +import { task } from 'hardhat/config'; +import '@nomiclabs/hardhat-ethers'; +import { getContractAt } from '@nomiclabs/hardhat-ethers/dist/src/helpers'; +import { config } from 'dotenv'; +import { IAaveGovernanceV2 } from '../types/IAaveGovernanceV2'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const bs58 = require('bs58'); + +config(); + +task('create:proposal-new-asset:alusd', 'Get the calldata to make a proposal to list ALUSD') + // eslint-disable-next-line no-empty-pattern + .setAction(async ({}, _DRE: any) => { + const { + TOKEN, + ATOKEN, + STABLE_DEBT_TOKEN, + VARIABLE_DEBT_TOKEN, + INTEREST_STRATEGY, + LTV, + LIQUIDATION_THRESHOLD, + LIQUIDATION_BONUS, + RESERVE_FACTOR, + DECIMALS, + ENABLE_BORROW, + ENABLE_AS_COLLATERAL, + ENABLE_STABLE_BORROW, + IPFS_HASH, + CHAINLINK_ORACLE_PROXY, + AAVE_GOVERNANCE_V2 = '0xEC568fffba86c094cf06b22134B23074DFE2252c', // mainnet + AAVE_SHORT_EXECUTOR = '0xee56e2b3d491590b5b31738cc34d5232f378a8d5', // mainnet + AAVE_PRICE_ORACLE_V2 = '0xA50ba011c48153De246E5192C8f9258A2ba79Ca9' // mainnet + } = process.env; + if ( + !TOKEN || + !ATOKEN || + !STABLE_DEBT_TOKEN || + !VARIABLE_DEBT_TOKEN || + !INTEREST_STRATEGY || + !LTV || + !LIQUIDATION_BONUS || + !LIQUIDATION_THRESHOLD || + !DECIMALS || + (ENABLE_BORROW !== 'true' && ENABLE_BORROW !== 'false') || + (ENABLE_AS_COLLATERAL !== 'true' && ENABLE_AS_COLLATERAL !== 'false') || + (ENABLE_STABLE_BORROW !== 'true' && ENABLE_STABLE_BORROW !== 'false') || + !IPFS_HASH || + !CHAINLINK_ORACLE_PROXY|| + !AAVE_GOVERNANCE_V2 || + !AAVE_SHORT_EXECUTOR || + !AAVE_PRICE_ORACLE_V2 || + !RESERVE_FACTOR + ) { + throw new Error('You have not set correctly the .env file, make sure to read the README.md'); + } + const proposer = (await _DRE.ethers.getSigners())[0]; + const genericPayloadAddress = ( + await _DRE.deployments.get('AssetListingProposalGenericExecutor') + ).address; + console.log(genericPayloadAddress) + const executeSignature = + 'execute(address,address,address,address,address,uint256,uint256,uint256,uint256,uint8,bool,bool,bool)'; + const executeCallData = _DRE.ethers.utils.defaultAbiCoder.encode( + [ + 'address', + 'address', + 'address', + 'address', + 'address', + 'uint', + 'uint', + 'uint', + 'uint', + 'uint8', + 'bool', + 'bool', + 'bool', + ], + [ + TOKEN, + ATOKEN, + STABLE_DEBT_TOKEN, + VARIABLE_DEBT_TOKEN, + INTEREST_STRATEGY, + LTV, + LIQUIDATION_THRESHOLD, + LIQUIDATION_BONUS, + RESERVE_FACTOR, + DECIMALS, + ENABLE_BORROW === 'true', + ENABLE_STABLE_BORROW === 'true', + ENABLE_AS_COLLATERAL === 'true', + ] + ); + + // Set the Chainlink oracle address + const setAssetSignature = 'setAssetSources(address[],address[])'; + const setAssetCallData = _DRE.ethers.utils.defaultAbiCoder.encode( + ['address[]', 'address[]'], + [[TOKEN], [CHAINLINK_ORACLE_PROXY]] + ); + + const gov = (await getContractAt( + _DRE, + 'IAaveGovernanceV2', + AAVE_GOVERNANCE_V2 || '' + )) as IAaveGovernanceV2; + const ipfsEncoded = `0x${bs58.decode(IPFS_HASH).slice(2).toString('hex')}`; + const tx = await gov + .connect(proposer) + .populateTransaction + .create( + AAVE_SHORT_EXECUTOR, + [genericPayloadAddress, AAVE_PRICE_ORACLE_V2], + ['0', '0'], + [executeSignature, setAssetSignature], + [executeCallData, setAssetCallData], + [true, false], + ipfsEncoded + ) + + console.log("Your Proposal:", tx); + + await (await proposer.sendTransaction(tx)).wait() + }); \ No newline at end of file diff --git a/test/test-listing-alusd.spec.ts b/test/test-listing-alusd.spec.ts new file mode 100644 index 0000000..d5fec25 --- /dev/null +++ b/test/test-listing-alusd.spec.ts @@ -0,0 +1,272 @@ + +import path from 'path'; +import { expect } from 'chai'; +import { config } from 'dotenv'; + +import rawBRE, { ethers } from 'hardhat'; + +import { BigNumber } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; +import { JsonRpcSigner } from '@ethersproject/providers'; +import { + evmSnapshot, + increaseTime, + evmRevert, + latestBlock, + advanceBlockTo, + impersonateAccountsHardhat, + MAX_UINT_AMOUNT, +} from './utils/utils'; +import { parsePoolData } from './utils/listing'; +import { IAaveGovernanceV2 } from '../types/IAaveGovernanceV2'; +import { IAaveOracle } from '../types/IAaveOracle'; +import { ILendingPool } from '../types/ILendingPool'; +import { SelfdestructTransferFactory } from '../types/SelfdestructTransferFactory' +import { IERC20 } from '../types/IERC20'; + +config({ path: path.resolve(process.cwd(), '.alusd.env') }); + +const { + TOKEN, + ATOKEN, + STABLE_DEBT_TOKEN, + VARIABLE_DEBT_TOKEN, + INTEREST_STRATEGY, + LTV, + LIQUIDATION_THRESHOLD, + LIQUIDATION_BONUS, + RESERVE_FACTOR, + DECIMALS, + IPFS_HASH, + AAVE_GOVERNANCE_V2 = '0xEC568fffba86c094cf06b22134B23074DFE2252c', // mainnet + AAVE_SHORT_EXECUTOR = '0xee56e2b3d491590b5b31738cc34d5232f378a8d5', // mainnet +} = process.env; + +if ( + !TOKEN || + !ATOKEN || + !STABLE_DEBT_TOKEN || + !VARIABLE_DEBT_TOKEN || + !INTEREST_STRATEGY || + !LTV || + !LIQUIDATION_BONUS || + !LIQUIDATION_THRESHOLD || + !DECIMALS || + !IPFS_HASH || + !AAVE_GOVERNANCE_V2 || + !AAVE_SHORT_EXECUTOR || + !RESERVE_FACTOR +) { + throw new Error('You have not set correctly the .env file, make sure to read the README.md'); +} + +const AAVE_LENDING_POOL = '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9'; +const VOTING_DURATION = 19200; + +const AAVE_WHALE = '0x25f2226b597e8f9514b3f68f00f494cf4f286491'; +const AAVE_TOKEN = '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9'; + +const ALUSD_HOLDER = '0xB05136FAcE3F8c6f9e6CDb076a38Ee6D4910f6d4'; +const AAVE_ORACLE = '0xA50ba011c48153De246E5192C8f9258A2ba79Ca9'; + +const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f'; +const DAI_HOLDER = '0xE78388b4CE79068e89Bf8aA7f218eF6b9AB0e9d0'; + +const ERRORS = { + NO_BORROW: '7', + NO_COLLATERAL_BALANCE: '9', + NO_STABLE_BORROW: '12', +}; + +describe('Deploy ALUSD assets with different params', () => { + let whale: JsonRpcSigner; + let alusdHolder: JsonRpcSigner; + let daiHolder: JsonRpcSigner; + let proposer: SignerWithAddress; + let gov: IAaveGovernanceV2; + let pool: ILendingPool; + let oracle: IAaveOracle; + let aave: IERC20; + let alusd: IERC20; + let dai: IERC20; + let aAlusd: IERC20; + let stableDebt: IERC20; + let variableDebt: IERC20; + let proposal: BigNumber; + let snapshotId: string; + let decimalMultiplier: BigNumber; + before(async () => { + console.log("before everything"); + [proposer] = await rawBRE.ethers.getSigners(); + // send ether to the AAVE_WHALE, which is a non payable contract. Via selfdestruct + await rawBRE.deployments.deploy('SelfdestructTransfer', { from: proposer.address }); + let selfDestructContract = await new SelfdestructTransferFactory(proposer).deploy(); + await ( + await selfDestructContract.destroyAndTransfer(AAVE_WHALE, { + value: ethers.utils.parseEther('1'), + }) + ).wait(); + selfDestructContract = await new SelfdestructTransferFactory(proposer).deploy(); + await ( + await selfDestructContract.destroyAndTransfer(ALUSD_HOLDER, { + value: ethers.utils.parseEther('1'), + }) + ).wait(); + await impersonateAccountsHardhat([AAVE_WHALE, ALUSD_HOLDER, DAI_HOLDER]); + + // impersonating holders + + whale = ethers.provider.getSigner(AAVE_WHALE); + alusdHolder = ethers.provider.getSigner(ALUSD_HOLDER); + daiHolder = ethers.provider.getSigner(DAI_HOLDER); + //getting main entry point contracts + gov = (await ethers.getContractAt( + 'IAaveGovernanceV2', + AAVE_GOVERNANCE_V2, + proposer + )) as IAaveGovernanceV2; + pool = (await ethers.getContractAt( + 'ILendingPool', + AAVE_LENDING_POOL, + proposer + )) as ILendingPool; + + // getting tokens used for tests + aave = (await ethers.getContractAt('IERC20', AAVE_TOKEN, whale)) as IERC20; + dai = (await ethers.getContractAt('IERC20', DAI_TOKEN, daiHolder)) as IERC20; + alusd = (await ethers.getContractAt('IERC20', TOKEN, alusdHolder)) as IERC20; + oracle = (await ethers.getContractAt('IAaveOracle', AAVE_ORACLE)) as IAaveOracle; + decimalMultiplier = BigNumber.from('10').pow(await alusd.decimals()); + + // Give alusd to whale + console.log("give alusd to whale"); + await ( + await aave.transfer( + proposer.address, + (await aave.balanceOf(AAVE_WHALE)).sub(parseEther('10000')) + ) + ).wait(); + + // giving just a bit of Dai to ALUSD holder to pay for interest later + console.log("give dai to alusd holder to pay for interest"); + await (await dai.transfer(ALUSD_HOLDER, parseEther('10'))).wait(); + console.log("transfer alusd to proposer"); + let alusdHolderBalance = await alusd.balanceOf(ALUSD_HOLDER); + let amount = (await alusd.balanceOf(ALUSD_HOLDER)).sub((parseEther('1000').div(decimalMultiplier))); + console.log(`alusd amount: ${amount}, holder: ${alusdHolderBalance}`); + await ( + await alusd.transfer( + proposer.address, + (await alusd.balanceOf(ALUSD_HOLDER)).sub((parseEther('1000').div(decimalMultiplier))) + ) + ).wait(); + + + // deploying the payload + console.log("deploy payload"); + await rawBRE.ethers.provider.send('evm_mine', [0]); + + await rawBRE.deployments.deploy('AssetListingProposalGenericExecutor', { + from: proposer.address, + gasLimit: 4000000, + gasPrice: BigNumber.from('75000000000'), + args: [], + }); + + + proposal = await gov.getProposalsCount(); + + await rawBRE.run('create:proposal-new-asset:alusd'); + + // voting, queuing proposals + await rawBRE.ethers.provider.send('evm_mine', [0]); + + await (await gov.submitVote(proposal, true)).wait(); + await advanceBlockTo((await latestBlock()) + VOTING_DURATION + 1); + await (await gov.queue(proposal)).wait(); + let proposalState = await gov.getProposalState(proposal); + expect(proposalState).to.be.equal(5); + await increaseTime(86400 + 10); + snapshotId = await evmSnapshot(); + }); + + it('Should list correctly an asset: borrow on, collateral off, stable borrow on', async () => { + + await (await gov.execute(proposal)).wait(); + const proposalState = await gov.getProposalState(proposal); + expect(proposalState).to.be.equal(7); + const { + configuration: { data }, + aTokenAddress, + stableDebtTokenAddress, + variableDebtTokenAddress, + } = await pool.getReserveData(TOKEN); + const poolData = parsePoolData(data); + expect(poolData).to.be.eql({ + reserveFactor: RESERVE_FACTOR, + reserved: '0', + stableRateEnabled: '1', + borrowingEnabled: '1', + reserveFrozen: '0', + reserveActive: '1', + decimals: DECIMALS, + liquidityBonus: '0', + LiquidityThreshold: '0', + LTV: '0', + }); + + // preparing for tests. + aAlusd = (await ethers.getContractAt('IERC20', aTokenAddress, proposer)) as IERC20; + stableDebt = (await ethers.getContractAt('IERC20', stableDebtTokenAddress, proposer)) as IERC20; + variableDebt = (await ethers.getContractAt( + 'IERC20', + variableDebtTokenAddress, + proposer + )) as IERC20; + await (await alusd.connect(alusdHolder).approve(pool.address, parseEther('200000'))).wait(); + await (await aave.connect(proposer).approve(pool.address, parseEther('200000'))).wait(); + + // AAVE deposit by proposer + await (await pool.deposit(aave.address, parseEther('100'), proposer.address, 0)).wait(); + // ALUSD deposit by alUSD holder + const depositedAmount = parseEther('100').div(decimalMultiplier); + await ( + await pool.connect(alusdHolder).deposit(alusd.address, depositedAmount, ALUSD_HOLDER, 0) + ).wait(); + expect(await aAlusd.balanceOf(ALUSD_HOLDER)).to.gte(depositedAmount.sub(1)); + expect(await aAlusd.balanceOf(ALUSD_HOLDER)).to.lte(depositedAmount.add(1)); + + // ALUSD holder not able to borrow DAI against ALUSD + await expect( + pool.connect(alusdHolder).borrow(dai.address, parseEther('1'), 2, 0, ALUSD_HOLDER) + ).to.be.revertedWith(ERRORS.NO_COLLATERAL_BALANCE); + + // proposer able to borrow ALUSD variable against AAVE + const borrowAmount = parseEther('10').div(decimalMultiplier); + await ( + await pool.connect(proposer).borrow(alusd.address, borrowAmount, 2, 0, proposer.address) + ).wait(); + expect(await variableDebt.balanceOf(proposer.address)).to.be.equal(borrowAmount); + increaseTime(40000); + + // proposer able to repay ALUSD variable + await (await alusd.connect(proposer).approve(pool.address, parseEther('100000'))).wait(); + await (await pool.repay(alusd.address, MAX_UINT_AMOUNT, 2, proposer.address)).wait(); + expect(await variableDebt.balanceOf(proposer.address)).to.be.equal(parseEther('0')); + + // proposer able to borrow ALUSD stable against AAVE + await ( + await pool.borrow(alusd.address, borrowAmount, 1, 0, proposer.address) + ).wait(); + increaseTime(40000); + + // propoer able to repay ALUSD stable + await (await pool.repay(alusd.address, MAX_UINT_AMOUNT, 1, proposer.address)).wait(); + expect(await stableDebt.balanceOf(proposer.address)).to.be.equal(parseEther('0')); + }); + + it("Oracle should return a non zero ALUSD price", async () => { + expect(await oracle.getAssetPrice(TOKEN)).to.be.gt('0') + }) +}); \ No newline at end of file