diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index cb7c016e..ea984f40 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -250,6 +250,102 @@ export const buildPermitParams = ( }, }); +export const buildMetaDepositParams = ( + chainId: number, + token: tEthereumAddress, + revision: string, + tokenName: string, + depositor: tEthereumAddress, + recipient: tEthereumAddress, + referralCode: number, + fromUnderlying: boolean, + nonce: number, + deadline: string, + value: tStringTokenSmallUnits +) => ({ + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Deposit: [ + { name: 'depositor', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'referralCode', type: 'uint16' }, + { name: 'fromUnderlying', type: 'bool' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Deposit' as const, + domain: { + name: tokenName, + version: revision, + chainId: chainId, + verifyingContract: token, + }, + message: { + depositor, + recipient, + value, + referralCode, + fromUnderlying, + nonce, + deadline, + }, +}); + +export const buildMetaWithdrawParams = ( + chainId: number, + token: tEthereumAddress, + revision: string, + tokenName: string, + owner: tEthereumAddress, + recipient: tEthereumAddress, + staticAmount: string, + dynamicAmount: string, + toUnderlying: boolean, + nonce: number, + deadline: string +) => ({ + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Withdraw: [ + { name: 'owner', type: 'address' }, + { name: 'recipient', type: 'address' }, + { name: 'staticAmount', type: 'uint256' }, + { name: 'dynamicAmount', type: 'uint256' }, + { name: 'toUnderlying', type: 'bool' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }, + primaryType: 'Withdraw' as const, + domain: { + name: tokenName, + version: revision, + chainId: chainId, + verifyingContract: token, + }, + message: { + owner, + recipient, + staticAmount, + dynamicAmount, + toUnderlying, + nonce, + deadline, + }, +}); + export const getSignatureFromTypedData = ( privateKey: string, typedData: any // TODO: should be TypedData, from eth-sig-utils, but TS doesn't accept it diff --git a/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining-rewards.spec.ts b/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining-rewards.spec.ts new file mode 100644 index 00000000..18f9ca0d --- /dev/null +++ b/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining-rewards.spec.ts @@ -0,0 +1,597 @@ +import rawDRE, { ethers } from 'hardhat'; +import bnjs from 'bignumber.js'; +import { solidity } from 'ethereum-waffle'; +import { + LendingPoolFactory, + WETH9Factory, + ATokenFactory, + ERC20, + LendingPool, + StaticATokenLMFactory, + ERC20Factory, + WETH9, + AToken, + StaticATokenLM, +} from '../../../types'; +import { + impersonateAccountsHardhat, + DRE, + waitForTx, + evmRevert, + evmSnapshot, + timeLatest, + advanceTimeAndBlock, +} from '../../../helpers/misc-utils'; +import { BigNumber, providers, Signer, utils } from 'ethers'; +import { MAX_UINT_AMOUNT } from '../../../helpers/constants'; +import { tEthereumAddress } from '../../../helpers/types'; +import { AbiCoder, formatEther, verifyTypedData } from 'ethers/lib/utils'; + +import { _TypedDataEncoder } from 'ethers/lib/utils'; + +import { expect, use } from 'chai'; +import { stat } from 'fs'; +import { getCurrentBlock } from '../../../helpers/contracts-helpers'; + +//use(solidity); + +const DEFAULT_GAS_LIMIT = 10000000; +const DEFAULT_GAS_PRICE = utils.parseUnits('100', 'gwei'); + +const defaultTxParams = { gasLimit: DEFAULT_GAS_LIMIT, gasPrice: DEFAULT_GAS_PRICE }; + +const LENDING_POOL = '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9'; + +const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; +const STKAAVE = '0x4da27a545c0c5B758a6BA100e3a049001de870f5'; +const AWETH = '0x030bA81f1c18d280636F32af80b9AAd02Cf0854e'; + +const getUserData = async (_users: Signer[], _debug = false, { staticAToken, stkAave }) => { + let usersData: { + pendingRewards: BigNumber; + stkAaveBalance: BigNumber; + staticBalance: BigNumber; + }[] = []; + if (_debug) { + console.log(`Printing user data:`); + } + for (let i = 0; i < _users.length; i++) { + const userAddress = await _users[i].getAddress(); + usersData.push({ + pendingRewards: await staticAToken.getClaimableRewards(userAddress), + stkAaveBalance: await stkAave.balanceOf(userAddress), + staticBalance: await staticAToken.balanceOf(userAddress), + }); + if (_debug) { + console.log( + `\tUser ${i} pendingRewards: ${formatEther( + usersData[i].pendingRewards + )}, stkAave balance: ${formatEther(usersData[i].stkAaveBalance)}, static bal: ${formatEther( + usersData[i].staticBalance + )} ` + ); + } + } + return usersData; +}; + +const DUST = 100; + +describe('StaticATokenLM: aToken wrapper with static balances and liquidity mining', () => { + let userSigner: providers.JsonRpcSigner; + let user2Signer: providers.JsonRpcSigner; + let lendingPool: LendingPool; + let weth: WETH9; + let aweth: AToken; + let stkAave: ERC20; + + let staticAToken: StaticATokenLM; + + let snap: string; + + before(async () => { + await rawDRE.run('set-DRE'); + + console.log(`Initial block number: ${await getCurrentBlock()}`); + + const [user1, user2] = await DRE.ethers.getSigners(); + userSigner = DRE.ethers.provider.getSigner(await user1.getAddress()); + user2Signer = DRE.ethers.provider.getSigner(await user2.getAddress()); + lendingPool = LendingPoolFactory.connect(LENDING_POOL, userSigner); + + weth = WETH9Factory.connect(WETH, userSigner); + aweth = ATokenFactory.connect(AWETH, userSigner); + stkAave = ERC20Factory.connect(STKAAVE, userSigner); + + staticAToken = await new StaticATokenLMFactory(userSigner).deploy( + LENDING_POOL, + AWETH, + 'Static Aave Interest Bearing WETH', + 'stataAAVE' + ); + + snap = await evmSnapshot(); + }); + + beforeEach(async () => { + await evmRevert(snap); + snap = await evmSnapshot(); + }); + + after(async () => { + await evmRevert(snap); + }); + + describe('Small checks', async () => { + it('Rewards increase at deposit, update and withdraw and set to 0 at claim', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; // Still need to figure out why this works :eyes: + + // Just preparation + await waitForTx(await weth.deposit({ value: amountToDeposit.mul(2) })); + await waitForTx( + await weth.approve(staticAToken.address, amountToDeposit.mul(2), defaultTxParams) + ); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const pendingRewards1 = await staticAToken.getClaimableRewards(userSigner._address); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const pendingRewards2 = await staticAToken.getClaimableRewards(userSigner._address); + + await waitForTx(await staticAToken.updateRewards()); + + const pendingRewards3 = await staticAToken.getClaimableRewards(userSigner._address); + + // Withdrawing all. + await waitForTx( + await staticAToken.withdraw(userSigner._address, amountToWithdraw, true, defaultTxParams) + ); + + const pendingRewards4 = await staticAToken.getClaimableRewards(userSigner._address); + const claimedRewards4 = await stkAave.balanceOf(userSigner._address); + + await waitForTx(await staticAToken.claimRewards(userSigner._address)); + + const pendingRewards5 = await staticAToken.getClaimableRewards(userSigner._address); + + expect(pendingRewards2).to.be.gt(pendingRewards1); + expect(pendingRewards3).to.be.gt(pendingRewards2); + expect(pendingRewards4).to.be.gt(pendingRewards3); + expect(pendingRewards5).to.be.eq(0); + }); + + it('Check getters', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; // Still need to figure out why this works :eyes: + + // Just preparation + await waitForTx(await weth.deposit({ value: amountToDeposit.mul(2) })); + await waitForTx( + await weth.approve(staticAToken.address, amountToDeposit.mul(2), defaultTxParams) + ); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const staticBalance = await staticAToken.balanceOf(userSigner._address); + const dynamicBalance = await staticAToken.dynamicBalanceOf(userSigner._address); + + const dynamicBalanceFromStatic = await staticAToken.staticToDynamicAmount(staticBalance); + const staticBalanceFromDynamic = await staticAToken.dynamicToStaticAmount(dynamicBalance); + + expect(staticBalance).to.be.eq(staticBalanceFromDynamic); + expect(dynamicBalance).to.be.eq(dynamicBalanceFromStatic); + }); + + it('Update and claim', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; // Still need to figure out why this works :eyes: + + // Just preparation + await waitForTx(await weth.deposit({ value: amountToDeposit.mul(2) })); + await waitForTx( + await weth.approve(staticAToken.address, amountToDeposit.mul(2), defaultTxParams) + ); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const pendingRewards1 = await staticAToken.getClaimableRewards(userSigner._address); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const pendingRewards2 = await staticAToken.getClaimableRewards(userSigner._address); + + await waitForTx(await staticAToken.updateRewards()); + + const pendingRewards3 = await staticAToken.getClaimableRewards(userSigner._address); + const claimedRewards3 = await stkAave.balanceOf(userSigner._address); + + await waitForTx(await staticAToken.updateAndClaimRewards(userSigner._address)); + + const pendingRewards4 = await staticAToken.getClaimableRewards(userSigner._address); + const claimedRewards4 = await stkAave.balanceOf(userSigner._address); + + expect(pendingRewards1).to.be.eq(0); + expect(pendingRewards2).to.be.gt(pendingRewards1); + expect(pendingRewards3).to.be.gt(pendingRewards2); + expect(pendingRewards4).to.be.eq(0); + + expect(claimedRewards3).to.be.eq(0); + expect(claimedRewards4).to.be.gt(pendingRewards3); + }); + + it('Withdraw to other user', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; + + const recipient = user2Signer._address; + + // Just preparation + await waitForTx(await weth.deposit({ value: amountToDeposit.mul(2) })); + await waitForTx( + await weth.approve(staticAToken.address, amountToDeposit.mul(2), defaultTxParams) + ); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const userPendingRewards1 = await staticAToken.getClaimableRewards(userSigner._address); + const recipientPendingRewards1 = await staticAToken.getClaimableRewards(recipient); + + // Withdrawing all + await waitForTx( + await staticAToken.withdraw(recipient, amountToWithdraw, true, defaultTxParams) + ); + + const userPendingRewards2 = await staticAToken.getClaimableRewards(userSigner._address); + const recipientPendingRewards2 = await staticAToken.getClaimableRewards(recipient); + + // Check that the recipient have gotten the rewards + expect(userPendingRewards2).to.be.gt(userPendingRewards1); + expect(recipientPendingRewards1).to.be.eq(0); + expect(recipientPendingRewards2).to.be.eq(0); + }); + + // Those that checks that subs could not be wrong or something other + }); + + it('Multiple users deposit WETH on stataWETH, wait 1 hour, update rewards, one user transfer, then claim and update rewards.', async () => { + // In this case, the recipient should have approx twice the rewards. + // Note that he has not held the 2x balance for this entire time, but only for one block. + // He have gotten this extra reward from the sender, because there was not a update prior. + + // Only diff here is if we wait, transfer, wait + + // 1. Deposit + // 2. Wait 3600 seconds + // 2-5. Update rewards + // 3. Transfer + // 4. Wait 3600 seconds + // 5. Claim rewards + // 6. Update rewards + + // When doing so, it should be clear that the recipient also gets the 'uncollected' rewards to the protocol that the value has accrued since last update. + // The thought is that since it is expensive to retrieve these rewards, a small holder may rather want to give away the extra rewards (if rewards < gas). + + const amountToDeposit = utils.parseEther('5'); + const allusers = await DRE.ethers.getSigners(); + const users = [allusers[0], allusers[1], allusers[2], allusers[3], allusers[4]]; + + const _debugUserData = false; + + for (let i = 0; i < 5; i++) { + let currentUser = users[i]; + // Preparation + await waitForTx(await weth.connect(currentUser).deposit({ value: amountToDeposit })); + await waitForTx( + await weth + .connect(currentUser) + .approve(staticAToken.address, amountToDeposit, defaultTxParams) + ); + + // Deposit + await waitForTx( + await staticAToken + .connect(currentUser) + .deposit(await currentUser.getAddress(), amountToDeposit, 0, true, defaultTxParams) + ); + } + + // Advance time to accrue significant rewards. + await advanceTimeAndBlock(60 * 60); + await staticAToken.updateRewards(); + + let staticATokenStkAaveBalInitial = await stkAave.balanceOf(staticAToken.address); + let usersDataInitial = await getUserData(users, _debugUserData, { staticAToken, stkAave }); + + await waitForTx( + await staticAToken + .connect(users[0]) + .transfer( + await users[1].getAddress(), + await staticAToken.balanceOf(await users[0].getAddress()) + ) + ); + + await advanceTimeAndBlock(60 * 60); + + for (let i = 0; i < 5; i++) { + await waitForTx(await staticAToken.claimRewards(await users[i].getAddress())); + } + + let staticATokenStkAaveBalAfterTransferAndClaim = await stkAave.balanceOf(staticAToken.address); + let usersDataAfterTransferAndClaim = await getUserData(users, _debugUserData, { + staticAToken, + stkAave, + }); + + await waitForTx(await staticAToken.updateRewards()); + + let staticATokenStkAaveBalFinal = await stkAave.balanceOf(staticAToken.address); + let usersDataFinal = await getUserData(users, _debugUserData, { staticAToken, stkAave }); + + // Time for checks + let pendingRewardsSumInitial = BigNumber.from(0); + let pendingRewardsSumAfter = BigNumber.from(0); + let pendingRewardsSumFinal = BigNumber.from(0); + for (let i = 0; i < 5; i++) { + expect(usersDataInitial[i].stkAaveBalance).to.be.eq(0); + // Everyone else than i == 1, should have no change in pending rewards. + // i == 1, will get additional rewards that have accrue + expect(usersDataAfterTransferAndClaim[i].stkAaveBalance).to.be.eq( + usersDataInitial[i].pendingRewards + ); + if (i > 1) { + // Expect initial static balance == after transfer == after claiming + expect(usersDataInitial[i].staticBalance).to.be.eq( + usersDataAfterTransferAndClaim[i].staticBalance + ); + expect(usersDataInitial[i].staticBalance).to.be.eq(usersDataFinal[i].staticBalance); + } + + pendingRewardsSumInitial = pendingRewardsSumInitial.add(usersDataInitial[i].pendingRewards); + pendingRewardsSumAfter = pendingRewardsSumAfter.add( + usersDataAfterTransferAndClaim[i].pendingRewards + ); + pendingRewardsSumFinal = pendingRewardsSumFinal.add(usersDataFinal[i].pendingRewards); + } + + // Expect user 0 to accrue zero fees after the transfer + expect(usersDataFinal[0].pendingRewards).to.be.eq(0); + expect(usersDataAfterTransferAndClaim[0].staticBalance).to.be.eq(0); + expect(usersDataFinal[0].staticBalance).to.be.eq(0); + + // Expect user 1 to have received funds + expect(usersDataAfterTransferAndClaim[1].staticBalance).to.be.eq( + usersDataInitial[1].staticBalance.add(usersDataInitial[0].staticBalance) + ); + /* + * Expect user 1 to have accrued more than twice in pending rewards. + * note that we get very little rewards in the transfer, because of the fresh update. + */ + // Expect the pending of user to be a lot + expect(usersDataFinal[1].pendingRewards).to.be.gt(usersDataFinal[2].pendingRewards.mul(2)); + // Expect his total fees to be almost 1.5 as large. Because of the small initial diff + expect(usersDataFinal[1].pendingRewards.add(usersDataFinal[1].stkAaveBalance)).to.be.gt( + usersDataFinal[2].pendingRewards.add(usersDataFinal[2].stkAaveBalance).mul(145).div(100) + ); + expect(usersDataFinal[1].pendingRewards.add(usersDataFinal[1].stkAaveBalance)).to.be.lt( + usersDataFinal[2].pendingRewards.add(usersDataFinal[2].stkAaveBalance).mul(155).div(100) + ); + + // Expect there to be excess stkAave in the contract. Expect it to be dust. This ensure that everyone can claim full amount of rewards. + expect(pendingRewardsSumInitial).to.be.lte(staticATokenStkAaveBalInitial); + expect(staticATokenStkAaveBalInitial.sub(pendingRewardsSumInitial)).to.be.lte(DUST); + + expect(pendingRewardsSumAfter).to.be.lte(staticATokenStkAaveBalAfterTransferAndClaim); + expect(staticATokenStkAaveBalAfterTransferAndClaim.sub(pendingRewardsSumAfter)).to.be.lte(DUST); + + expect(pendingRewardsSumFinal).to.be.lte(staticATokenStkAaveBalFinal); + expect(staticATokenStkAaveBalFinal.sub(pendingRewardsSumFinal)).to.be.lte(DUST); + + expect(usersDataInitial[0].pendingRewards).to.be.eq( + usersDataAfterTransferAndClaim[0].stkAaveBalance + ); + expect(usersDataAfterTransferAndClaim[0].pendingRewards).to.be.eq(0); + expect(usersDataAfterTransferAndClaim[1].staticBalance).to.be.eq( + usersDataInitial[1].staticBalance.add(usersDataInitial[0].staticBalance) + ); + }); + + it('Multiple users deposit WETH on stataWETH, wait 1 hour, one user transfer, then claim and update rewards.', async () => { + // In this case, the recipient should have approx twice the rewards. + // Note that he has not held the 2x balance for this entire time, but only for one block. + // He have gotten this extra reward from the sender, because there was not a update prior. + + // Only diff here is if we wait, transfer, wait + + // 1. Deposit + // 2. Wait 3600 seconds + // 3. Transfer + // 4. Wait 3600 seconds + // 5. Claim rewards + // 6. Update rewards + + // When doing so, it should be clear that the recipient also gets the 'uncollected' rewards to the protocol that the value has accrued since last update. + // The thought is that since it is expensive to retrieve these rewards, a small holder may rather want to give away the extra rewards (if rewards < gas). + + const amountToDeposit = utils.parseEther('5'); //'5'); + const allusers = await DRE.ethers.getSigners(); + const users = [allusers[0], allusers[1], allusers[2], allusers[3], allusers[4]]; + + const _debugUserData = false; + + for (let i = 0; i < 5; i++) { + let currentUser = users[i]; + // Preparation + await waitForTx(await weth.connect(currentUser).deposit({ value: amountToDeposit })); + await waitForTx( + await weth + .connect(currentUser) + .approve(staticAToken.address, amountToDeposit, defaultTxParams) + ); + + // Deposit + await waitForTx( + await staticAToken + .connect(currentUser) + .deposit(await currentUser.getAddress(), amountToDeposit, 0, true, defaultTxParams) + ); + } + + // Advance time to accrue significant rewards. + await advanceTimeAndBlock(60 * 60); + + let staticATokenStkAaveBalInitial = await stkAave.balanceOf(staticAToken.address); + let usersDataInitial = await getUserData(users, _debugUserData, { staticAToken, stkAave }); + + // User 0 transfer full balance of staticATokens to user 1. This will also transfer the rewards since last update as well. + await waitForTx( + await staticAToken + .connect(users[0]) + .transfer( + await users[1].getAddress(), + await staticAToken.balanceOf(await users[0].getAddress()) + ) + ); + + await advanceTimeAndBlock(60 * 60); + + for (let i = 0; i < 5; i++) { + await waitForTx(await staticAToken.claimRewards(await users[i].getAddress())); + } + + let staticATokenStkAaveBalAfterTransfer = await stkAave.balanceOf(staticAToken.address); + let usersDataAfterTransfer = await getUserData(users, _debugUserData, { + staticAToken, + stkAave, + }); + + await waitForTx(await staticAToken.updateRewards()); + + let staticATokenStkAaveBalFinal = await stkAave.balanceOf(staticAToken.address); + let usersDataFinal = await getUserData(users, _debugUserData, { staticAToken, stkAave }); + + // Time for checks + let pendingRewardsSumInitial = BigNumber.from(0); + let pendingRewardsSumAfter = BigNumber.from(0); + let pendingRewardsSumFinal = BigNumber.from(0); + for (let i = 0; i < 5; i++) { + expect(usersDataInitial[i].stkAaveBalance).to.be.eq(0); + // Everyone else than i == 1, should have no change in pending rewards. + // i == 1, will get additional rewards that have accrue + expect(usersDataAfterTransfer[i].stkAaveBalance).to.be.eq(usersDataInitial[i].pendingRewards); + if (i > 1) { + // Expect initial static balance == after transfer == after claiming + expect(usersDataInitial[i].staticBalance).to.be.eq(usersDataAfterTransfer[i].staticBalance); + expect(usersDataInitial[i].staticBalance).to.be.eq(usersDataFinal[i].staticBalance); + } + + pendingRewardsSumInitial = pendingRewardsSumInitial.add(usersDataInitial[i].pendingRewards); + pendingRewardsSumAfter = pendingRewardsSumAfter.add(usersDataAfterTransfer[i].pendingRewards); + pendingRewardsSumFinal = pendingRewardsSumFinal.add(usersDataFinal[i].pendingRewards); + } + + // Expect user 0 to accrue zero fees after the transfer + expect(usersDataFinal[0].pendingRewards).to.be.eq(0); + expect(usersDataAfterTransfer[0].staticBalance).to.be.eq(0); + expect(usersDataFinal[0].staticBalance).to.be.eq(0); + + // Expect user 1 to have received funds + expect(usersDataAfterTransfer[1].staticBalance).to.be.eq( + usersDataInitial[1].staticBalance.add(usersDataInitial[0].staticBalance) + ); + /* + * Expect user 1 to have pending more than twice the rewards as the last user. + * Note that he should have accrued this, even though he did not have 2x bal for the full time, + * as he also received the "uncollected" rewards from user1 at the transfer. + */ + expect(usersDataFinal[1].pendingRewards).to.be.gt(usersDataFinal[2].pendingRewards.mul(2)); + // Expect his total fees to be almost twice as large. Because of the small initial diff + expect(usersDataFinal[1].pendingRewards.add(usersDataFinal[1].stkAaveBalance)).to.be.gt( + usersDataFinal[2].pendingRewards.add(usersDataFinal[2].stkAaveBalance).mul(195).div(100) + ); + expect(usersDataFinal[1].pendingRewards.add(usersDataFinal[1].stkAaveBalance)).to.be.lt( + usersDataFinal[2].pendingRewards.add(usersDataFinal[2].stkAaveBalance).mul(205).div(100) + ); + + // Expect there to be excess stkAave in the contract. + // Expect it to be dust. This ensure that everyone can claim full amount of rewards. + expect(pendingRewardsSumInitial).to.be.lte(staticATokenStkAaveBalInitial); + expect(staticATokenStkAaveBalInitial.sub(pendingRewardsSumInitial)).to.be.lte(DUST); + + expect(pendingRewardsSumAfter).to.be.lte(staticATokenStkAaveBalAfterTransfer); + expect(staticATokenStkAaveBalAfterTransfer.sub(pendingRewardsSumAfter)).to.be.lte(DUST); + + // We got an error here, pendingRewardsSumFinal = actual + 1 + expect(pendingRewardsSumFinal).to.be.lte(staticATokenStkAaveBalFinal); + expect(staticATokenStkAaveBalFinal.sub(pendingRewardsSumFinal)).to.be.lte(DUST); // How small should we say dust is? + + // Expect zero rewards after all is claimed. But there is some dust left. + expect(usersDataInitial[0].pendingRewards).to.be.eq(usersDataAfterTransfer[0].stkAaveBalance); + expect(usersDataAfterTransfer[0].pendingRewards).to.be.eq(0); + expect(usersDataAfterTransfer[1].staticBalance).to.be.eq( + usersDataInitial[1].staticBalance.add(usersDataInitial[0].staticBalance) + ); + }); + + it('Mass deposit, then mass claim', async () => { + const amountToDeposit = utils.parseEther('1.1'); // 18 decimals should be the worst here //1.135359735917531199 + const users = await DRE.ethers.getSigners(); + + const depositCount = 50; + + for (let i = 0; i < depositCount; i++) { + if (i % 50 == 0 && i > 0) { + console.log('50 deposits'); + } + let currentUser = users[i % users.length]; + // Preparation + await waitForTx(await weth.connect(currentUser).deposit({ value: amountToDeposit })); + await waitForTx( + await weth + .connect(currentUser) + .approve(staticAToken.address, amountToDeposit, defaultTxParams) + ); + + // Deposit + await waitForTx( + await staticAToken + .connect(currentUser) + .deposit(await currentUser.getAddress(), amountToDeposit, 0, true, defaultTxParams) + ); + } + + // Advance time to accrue significant rewards. + await advanceTimeAndBlock(60 * 60); + await waitForTx(await staticAToken.updateRewards()); + + for (let i = 0; i < users.length; i++) { + const pendingReward = await staticAToken.getClaimableRewards(await users[i].getAddress()); + await waitForTx(await staticAToken.claimRewards(await users[i].getAddress())); + // We have a mistake here. Rounding of the pendingReward seems to be the issue + // console.log(`What user: ${i}, with pending reward: ${pendingReward}`); + expect(await stkAave.balanceOf(await users[i].getAddress())).to.be.eq(pendingReward); + } + expect(await stkAave.balanceOf(staticAToken.address)).to.be.lt(DUST); + }); +}); diff --git a/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining.spec.ts b/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining.spec.ts new file mode 100644 index 00000000..93dcd90b --- /dev/null +++ b/test-suites/test-aave/mainnet/static-atoken-lm/static-atoken-liquidity-mining.spec.ts @@ -0,0 +1,948 @@ +import rawDRE, { ethers } from 'hardhat'; +import bnjs from 'bignumber.js'; +import { solidity } from 'ethereum-waffle'; +import { + LendingPoolFactory, + WETH9Factory, + StaticATokenFactory, + ATokenFactory, + ERC20, + LendingPool, + StaticATokenLMFactory, + ERC20Factory, + WETH9, + AToken, + StaticAToken, + StaticATokenLM, +} from '../../../types'; +import { + impersonateAccountsHardhat, + DRE, + waitForTx, + evmRevert, + evmSnapshot, + timeLatest, + advanceTimeAndBlock, +} from '../../../helpers/misc-utils'; +import { BigNumber, providers, Signer, utils } from 'ethers'; +import { rayMul } from '../../../helpers/ray-math'; +import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../../../helpers/constants'; +import { tEthereumAddress } from '../../../helpers/types'; +import { AbiCoder, formatEther, verifyTypedData } from 'ethers/lib/utils'; +import { stat } from 'fs'; + +import { _TypedDataEncoder } from 'ethers/lib/utils'; +import { + buildMetaDepositParams, + buildMetaWithdrawParams, + buildPermitParams, + getSignatureFromTypedData, +} from '../../../helpers/contracts-helpers'; +import { TypedDataUtils, typedSignatureHash, TYPED_MESSAGE_SCHEMA } from 'eth-sig-util'; +import { zeroAddress } from 'ethereumjs-util'; + +const { expect, use } = require('chai'); + +use(solidity); + +const DEFAULT_GAS_LIMIT = 10000000; +const DEFAULT_GAS_PRICE = utils.parseUnits('100', 'gwei'); + +const defaultTxParams = { gasLimit: DEFAULT_GAS_LIMIT, gasPrice: DEFAULT_GAS_PRICE }; + +const ETHER_BANK = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; +const LENDING_POOL = '0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9'; + +const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; +const STKAAVE = '0x4da27a545c0c5B758a6BA100e3a049001de870f5'; +const AWETH = '0x030bA81f1c18d280636F32af80b9AAd02Cf0854e'; + +const TEST_USERS = [ + '0x0F4ee9631f4be0a63756515141281A3E2B293Bbe', + '0x8BffC896D42F07776561A5814D6E4240950d6D3a', +]; + +type tBalancesInvolved = { + staticATokenATokenBalance: BigNumber; + staticATokenStkAaveBalance: BigNumber; + staticATokenUnderlyingBalance: BigNumber; + userStkAaveBalance: BigNumber; + userATokenBalance: BigNumber; + userUnderlyingBalance: BigNumber; + userStaticATokenBalance: BigNumber; + userDynamicStaticATokenBalance: BigNumber; + userPendingRewards: BigNumber; + user2StkAaveBalance: BigNumber; + user2ATokenBalance: BigNumber; + user2UnderlyingBalance: BigNumber; + user2StaticATokenBalance: BigNumber; + user2DynamicStaticATokenBalance: BigNumber; + user2PendingRewards: BigNumber; + currentRate: BigNumber; + staticATokenSupply: BigNumber; +}; + +type tContextParams = { + staticAToken: StaticATokenLM; + underlying: ERC20; + aToken: ERC20; + stkAave: ERC20; + user: tEthereumAddress; + user2: tEthereumAddress; + lendingPool: LendingPool; +}; + +const getContext = async ({ + staticAToken, + underlying, + aToken, + stkAave, + user, + user2, + lendingPool, +}: tContextParams): Promise => ({ + staticATokenATokenBalance: await aToken.balanceOf(staticAToken.address), + staticATokenStkAaveBalance: await stkAave.balanceOf(staticAToken.address), + staticATokenUnderlyingBalance: await underlying.balanceOf(staticAToken.address), + userStaticATokenBalance: await staticAToken.balanceOf(user), + userStkAaveBalance: await stkAave.balanceOf(user), + userATokenBalance: await aToken.balanceOf(user), + userUnderlyingBalance: await underlying.balanceOf(user), + userDynamicStaticATokenBalance: await staticAToken.dynamicBalanceOf(user), + userPendingRewards: await staticAToken.getClaimableRewards(user), + user2StkAaveBalance: await stkAave.balanceOf(user2), + user2ATokenBalance: await aToken.balanceOf(user2), + user2UnderlyingBalance: await underlying.balanceOf(user2), + user2StaticATokenBalance: await staticAToken.balanceOf(user2), + user2DynamicStaticATokenBalance: await staticAToken.dynamicBalanceOf(user2), + user2PendingRewards: await staticAToken.getClaimableRewards(user2), + currentRate: await lendingPool.getReserveNormalizedIncome(WETH), + staticATokenSupply: await staticAToken.totalSupply(), +}); + +describe('StaticATokenLM: aToken wrapper with static balances and liquidity mining', () => { + let userSigner: providers.JsonRpcSigner; + let user2Signer: providers.JsonRpcSigner; + let lendingPool: LendingPool; + let weth: WETH9; + let aweth: AToken; + let stkAave: ERC20; + + let staticAToken: StaticATokenLM; + + let snap: string; + + let ctxtParams: tContextParams; + + before(async () => { + await rawDRE.run('set-DRE'); + + const [user1, user2] = await DRE.ethers.getSigners(); + userSigner = DRE.ethers.provider.getSigner(await user1.getAddress()); + user2Signer = DRE.ethers.provider.getSigner(await user2.getAddress()); + lendingPool = LendingPoolFactory.connect(LENDING_POOL, userSigner); + + weth = WETH9Factory.connect(WETH, userSigner); + aweth = ATokenFactory.connect(AWETH, userSigner); + stkAave = ERC20Factory.connect(STKAAVE, userSigner); + + staticAToken = await new StaticATokenLMFactory(userSigner).deploy( + LENDING_POOL, + AWETH, + 'Static Aave Interest Bearing WETH', + 'stataAAVE' + ); + + ctxtParams = { + staticAToken: staticAToken, + underlying: (weth), + aToken: aweth, + stkAave: stkAave, + user: userSigner._address, + user2: user2Signer._address, + lendingPool, + }; + + snap = await evmSnapshot(); + }); + + beforeEach(async () => { + await evmRevert(snap); + snap = await evmSnapshot(); + }); + + after(async () => { + await evmRevert(snap); + }); + + it('Deposit WETH on stataWETH, then withdraw of the whole balance in underlying', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; // Still need to figure out why this works :eyes: + + // Just preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + const ctxtInitial = await getContext(ctxtParams); + + await expect( + staticAToken.deposit(ZERO_ADDRESS, amountToDeposit, 0, true, defaultTxParams) + ).to.be.revertedWith('INVALID_RECIPIENT'); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + + /* console.log( + `ScaledBalanceOf ${formatEther( + await aweth.scaledBalanceOf(staticAToken.address) + )}... Static supply: ${formatEther(await staticAToken.totalSupply())}... ${formatEther( + await staticAToken.balanceOf(userSigner._address) + )} ` + );*/ + + await expect( + staticAToken.withdraw(ZERO_ADDRESS, amountToWithdraw, true, defaultTxParams) + ).to.be.revertedWith('INVALID_RECIPIENT'); + + // Withdrawing all + await waitForTx( + await staticAToken.withdraw(userSigner._address, amountToWithdraw, true, defaultTxParams) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + + // Claiming the rewards + await waitForTx(await staticAToken.claimRewards(userSigner._address)); + + const ctxtAfterClaim = await getContext(ctxtParams); + + // Check values throughout + + // Check that aWETH balance of staticAToken contract is increased as expected + expect(ctxtAfterDeposit.staticATokenATokenBalance).to.be.eq( + ctxtInitial.staticATokenATokenBalance.add(amountToDeposit) + ); + + // Check user WETH balance of user is decreased as expected + expect(ctxtAfterDeposit.userUnderlyingBalance).to.be.eq( + ctxtInitial.userUnderlyingBalance.sub(amountToDeposit) + ); + /*console.log( + `Deposit amount ${formatEther(amountToDeposit)}. Dynamic balance: ${formatEther( + ctxtAfterDeposit.userDynamicStaticATokenBalance + )}. aWEth bal: ${formatEther(ctxtAfterDeposit.staticATokenATokenBalance)}` + );*/ + expect(ctxtAfterDeposit.userDynamicStaticATokenBalance).to.be.eq( + ctxtInitial.userDynamicStaticATokenBalance.add(amountToDeposit) + ); + expect(ctxtAfterDeposit.userDynamicStaticATokenBalance).to.be.eq( + ctxtAfterDeposit.staticATokenATokenBalance + ); + expect(ctxtAfterDeposit.staticATokenUnderlyingBalance).to.be.eq( + ctxtInitial.staticATokenUnderlyingBalance + ); + expect(ctxtAfterDeposit.userATokenBalance).to.be.eq(ctxtInitial.userATokenBalance); + expect(ctxtAfterDeposit.userStkAaveBalance).to.be.eq(0); + expect(ctxtAfterDeposit.staticATokenStkAaveBalance).to.be.eq(0); + + expect( + ctxtAfterWithdrawal.staticATokenATokenBalance, + 'INVALID_ATOKEN_BALANCE_ON_STATICATOKEN_AFTER_WITHDRAW' + ).to.be.eq( + BigNumber.from( + rayMul( + new bnjs( + ctxtAfterWithdrawal.staticATokenSupply + .add(ctxtAfterDeposit.userStaticATokenBalance) + .toString() + ), + new bnjs(ctxtAfterWithdrawal.currentRate.toString()) + ) + .minus( + rayMul( + new bnjs(ctxtAfterDeposit.userStaticATokenBalance.toString()), + new bnjs(ctxtAfterWithdrawal.currentRate.toString()) + ) + ) + .toString() + ) + ); + + expect(ctxtAfterWithdrawal.userStaticATokenBalance).to.be.eq(0); + expect(ctxtAfterWithdrawal.staticATokenSupply).to.be.eq(0); + expect(ctxtAfterWithdrawal.staticATokenUnderlyingBalance).to.be.eq(0); + + // Check with possible rounding error. Sometimes we have an issue with it being 0 lower as well. + expect(ctxtAfterWithdrawal.staticATokenStkAaveBalance).to.be.gte( + ctxtAfterWithdrawal.userPendingRewards + ); + expect(ctxtAfterWithdrawal.staticATokenStkAaveBalance).to.be.lte( + ctxtAfterWithdrawal.userPendingRewards.add(1) + ); + expect(ctxtAfterWithdrawal.userStkAaveBalance).to.be.eq(0); + + expect(ctxtAfterClaim.userStkAaveBalance).to.be.eq(ctxtAfterWithdrawal.userPendingRewards); + expect(ctxtAfterClaim.staticATokenStkAaveBalance).to.be.lte(1); + }); + + it('Deposit WETH on stataWETH and then withdraw some balance in underlying', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = utils.parseEther('2.5'); + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + const ctxtInitial = await getContext(ctxtParams); + + // Deposit + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + + // Withdraw + await waitForTx( + await staticAToken.withdraw(userSigner._address, amountToWithdraw, true, defaultTxParams) + ); + const ctxtAfterWithdrawal = await getContext(ctxtParams); + + // Claim + await waitForTx(await staticAToken.claimRewards(userSigner._address)); + const ctxtAfterClaim = await getContext(ctxtParams); + }); + + it('Deposit WETH on stataWETH and then withdraw all the balance in aToken', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; // Still need to figure out why this works :eyes: + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + const ctxtInitial = await getContext(ctxtParams); + + // Deposit + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + + // Withdraw + await waitForTx( + await staticAToken.withdraw(userSigner._address, amountToWithdraw, false, defaultTxParams) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + + // Claim + await waitForTx(await staticAToken.claimRewards(userSigner._address)); + const ctxtAfterClaim = await getContext(ctxtParams); + }); + + it('Deposit aWETH on stataWETH and then withdraw some balance in aToken', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = utils.parseEther('2.5'); + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(lendingPool.address, amountToDeposit, defaultTxParams)); + await waitForTx( + await lendingPool.deposit( + weth.address, + amountToDeposit, + userSigner._address, + 0, + defaultTxParams + ) + ); + await waitForTx(await aweth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + const ctxtInitial = await getContext(ctxtParams); + + // Deposit + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, false, defaultTxParams) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + + // Withdraw + await waitForTx( + await staticAToken.withdraw(userSigner._address, amountToWithdraw, false, defaultTxParams) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + + // Claim + await waitForTx(await staticAToken.claimRewards(userSigner._address)); + const ctxtAfterClaim = await getContext(ctxtParams); + }); + + it('Transfer with permit() (expect fail)', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; // Still need to figure out why this works :eyes: + + // Just preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const ownerPrivateKey = require('../../../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const owner = userSigner; + const spender = user2Signer; + + const tokenName = await staticAToken.name(); + + const chainId = DRE.network.config.chainId || 1; + const expiration = 0; + const nonce = (await staticAToken._nonces(owner._address)).toNumber(); + const permitAmount = ethers.utils.parseEther('2').toString(); + const msgParams = buildPermitParams( + chainId, + staticAToken.address, + '1', + tokenName, + owner._address, + spender._address, + nonce, + expiration.toFixed(), + permitAmount + ); + + expect((await staticAToken.allowance(owner._address, spender._address)).toString()).to.be.equal( + '0', + 'INVALID_ALLOWANCE_BEFORE_PERMIT' + ); + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + staticAToken + .connect(spender) + .permit(ZERO_ADDRESS, spender._address, permitAmount, expiration, v, r, s, chainId) + ).to.be.revertedWith('INVALID_OWNER'); + + await expect( + staticAToken + .connect(spender) + .permit(owner._address, spender._address, permitAmount, expiration, v, r, s, chainId) + ).to.be.revertedWith('INVALID_EXPIRATION'); + + expect((await staticAToken.allowance(owner._address, spender._address)).toString()).to.be.equal( + '0', + 'INVALID_ALLOWANCE_AFTER_PERMIT' + ); + }); + + it('Transfer with permit()', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; // Still need to figure out why this works :eyes: + + // Just preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + // Depositing + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const ownerPrivateKey = require('../../../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const owner = userSigner; + const spender = user2Signer; + + const tokenName = await staticAToken.name(); + + const chainId = DRE.network.config.chainId || 1; + const expiration = MAX_UINT_AMOUNT; + const nonce = (await staticAToken._nonces(owner._address)).toNumber(); + const permitAmount = ethers.utils.parseEther('2').toString(); + const msgParams = buildPermitParams( + chainId, + staticAToken.address, + '1', + tokenName, + owner._address, + spender._address, + nonce, + expiration, + permitAmount + ); + + expect((await staticAToken.allowance(owner._address, spender._address)).toString()).to.be.equal( + '0', + 'INVALID_ALLOWANCE_BEFORE_PERMIT' + ); + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + staticAToken + .connect(spender) + .permit(spender._address, spender._address, permitAmount, expiration, v, r, s, chainId) + ).to.be.revertedWith('INVALID_SIGNATURE'); + + await waitForTx( + await staticAToken + .connect(spender) + .permit(owner._address, spender._address, permitAmount, expiration, v, r, s, chainId) + ); + + expect((await staticAToken.allowance(owner._address, spender._address)).toString()).to.be.equal( + permitAmount, + 'INVALID_ALLOWANCE_AFTER_PERMIT' + ); + }); + + it('Deposit using metaDeposit()', async () => { + // What is a metadeposit + const amountToDeposit = utils.parseEther('5'); + const chainId = DRE.network.config.chainId ? DRE.network.config.chainId : 1; + + const domain = { + name: await staticAToken.name(), + version: '1', + chainId: chainId, + verifyingContract: staticAToken.address, + }; + const domainSeperator = _TypedDataEncoder.hashDomain(domain); + const seperator = await staticAToken.getDomainSeparator(chainId); + expect(seperator).to.be.eq(domainSeperator); + + const userPrivateKey = require('../../../test-wallets.js').accounts[0].secretKey; + if (!userPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + // Here it begins + const tokenName = await staticAToken.name(); + const nonce = (await staticAToken._nonces(userSigner._address)).toNumber(); + const value = amountToDeposit.toString(); + const referralCode = 0; + const depositor = userSigner._address; + const recipient = userSigner._address; + const fromUnderlying = true; + const deadline = MAX_UINT_AMOUNT; // (await timeLatest()).plus(60 * 60).toFixed(); + + const msgParams = buildMetaDepositParams( + chainId, + staticAToken.address, + '1', + tokenName, + depositor, + recipient, + referralCode, + fromUnderlying, + nonce, + deadline, + value + ); + + const { v, r, s } = getSignatureFromTypedData(userPrivateKey, msgParams); + + const sigParams = { + v, + r, + s, + }; + + const ctxtInitial = await getContext(ctxtParams); + + await expect( + staticAToken + .connect(user2Signer) + .metaDeposit( + ZERO_ADDRESS, + recipient, + value, + referralCode, + fromUnderlying, + deadline, + sigParams, + chainId + ) + ).to.be.revertedWith('INVALID_DEPOSITOR'); + + await expect( + staticAToken + .connect(user2Signer) + .metaDeposit( + depositor, + recipient, + value, + referralCode, + fromUnderlying, + 0, + sigParams, + chainId + ) + ).to.be.revertedWith('INVALID_EXPIRATION'); + + await expect( + staticAToken + .connect(user2Signer) + .metaDeposit( + user2Signer._address, + recipient, + value, + referralCode, + fromUnderlying, + deadline, + sigParams, + chainId + ) + ).to.be.revertedWith('INVALID_SIGNATURE'); + + // Deposit + await waitForTx( + await staticAToken + .connect(user2Signer) + .metaDeposit( + depositor, + recipient, + value, + referralCode, + fromUnderlying, + deadline, + sigParams, + chainId + ) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + }); + + it('Withdraw using withdrawDynamicAmount()', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = utils.parseEther('1'); + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + const ctxtInitial = await getContext(ctxtParams); + + // Deposit + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + + // Withdraw dynamic amount + await waitForTx( + await staticAToken.withdrawDynamicAmount( + userSigner._address, + amountToWithdraw, + false, + defaultTxParams + ) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + + // Claim + await waitForTx(await staticAToken.claimRewards(userSigner._address)); + const ctxtAfterClaim = await getContext(ctxtParams); + }); + + it('Withdraw using metaWithdraw()', async () => { + // What is a metadeposit + const amountToDeposit = utils.parseEther('5'); + const chainId = DRE.network.config.chainId ? DRE.network.config.chainId : 1; + + const domain = { + name: await staticAToken.name(), + version: '1', + chainId: chainId, + verifyingContract: staticAToken.address, + }; + const domainSeperator = _TypedDataEncoder.hashDomain(domain); + const seperator = await staticAToken.getDomainSeparator(chainId); + expect(seperator).to.be.eq(domainSeperator); + + const userPrivateKey = require('../../../test-wallets.js').accounts[0].secretKey; + if (!userPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + // Deposit + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + // Meta withdraw + const tokenName = await staticAToken.name(); + const nonce = (await staticAToken._nonces(userSigner._address)).toNumber(); + const owner = userSigner._address; + const recipient = userSigner._address; + const staticAmount = (await staticAToken.balanceOf(userSigner._address)).toString(); + const dynamicAmount = '0'; + const toUnderlying = true; + const deadline = MAX_UINT_AMOUNT; // (await timeLatest()).plus(60 * 60).toFixed(); + + const msgParams = buildMetaWithdrawParams( + chainId, + staticAToken.address, + '1', + tokenName, + owner, + recipient, + staticAmount, + dynamicAmount, + toUnderlying, + nonce, + deadline + ); + + const { v, r, s } = getSignatureFromTypedData(userPrivateKey, msgParams); + + const sigParams = { + v, + r, + s, + }; + + const ctxtInitial = await getContext(ctxtParams); + + await expect( + staticAToken + .connect(user2Signer) + .metaWithdraw( + ZERO_ADDRESS, + recipient, + staticAmount, + dynamicAmount, + toUnderlying, + deadline, + sigParams, + chainId + ) + ).to.be.revertedWith('INVALID_OWNER'); + + await expect( + staticAToken + .connect(user2Signer) + .metaWithdraw( + owner, + recipient, + staticAmount, + dynamicAmount, + toUnderlying, + 0, + sigParams, + chainId + ) + ).to.be.revertedWith('INVALID_EXPIRATION'); + + await expect( + staticAToken + .connect(user2Signer) + .metaWithdraw( + user2Signer._address, + recipient, + staticAmount, + dynamicAmount, + toUnderlying, + deadline, + sigParams, + chainId + ) + ).to.be.revertedWith('INVALID_SIGNATURE'); + + // Deposit + await waitForTx( + await staticAToken + .connect(user2Signer) + .metaWithdraw( + owner, + recipient, + staticAmount, + dynamicAmount, + toUnderlying, + deadline, + sigParams, + chainId + ) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + }); + + it('Withdraw using metaWithdraw() (expect to fail)', async () => { + // What is a metadeposit + const amountToDeposit = utils.parseEther('5'); + const chainId = DRE.network.config.chainId ? DRE.network.config.chainId : 1; + + const domain = { + name: await staticAToken.name(), + version: '1', + chainId: chainId, + verifyingContract: staticAToken.address, + }; + const domainSeperator = _TypedDataEncoder.hashDomain(domain); + const seperator = await staticAToken.getDomainSeparator(chainId); + expect(seperator).to.be.eq(domainSeperator); + + const userPrivateKey = require('../../../test-wallets.js').accounts[0].secretKey; + if (!userPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + // Deposit + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + // Meta withdraw + const tokenName = await staticAToken.name(); + const nonce = (await staticAToken._nonces(userSigner._address)).toNumber(); + const owner = userSigner._address; + const recipient = userSigner._address; + const staticAmount = (await staticAToken.balanceOf(userSigner._address)).toString(); + const dynamicAmount = ( + await await staticAToken.dynamicBalanceOf(userSigner._address) + ).toString(); + const toUnderlying = true; + const deadline = MAX_UINT_AMOUNT; // (await timeLatest()).plus(60 * 60).toFixed(); + + const msgParams = buildMetaWithdrawParams( + chainId, + staticAToken.address, + '1', + tokenName, + owner, + recipient, + staticAmount, + dynamicAmount, + toUnderlying, + nonce, + deadline + ); + + const { v, r, s } = getSignatureFromTypedData(userPrivateKey, msgParams); + + const sigParams = { + v, + r, + s, + }; + + const ctxtInitial = await getContext(ctxtParams); + + await expect( + staticAToken + .connect(user2Signer) + .metaWithdraw( + owner, + recipient, + staticAmount, + dynamicAmount, + toUnderlying, + deadline, + sigParams, + chainId + ) + ).to.be.revertedWith('ONLY_ONE_AMOUNT_FORMAT_ALLOWED'); + + const ctxtAfterDeposit = await getContext(ctxtParams); + }); + + it('Deposit WETH on stataWETH, then transfer and withdraw of the whole balance in underlying, finally claim', async () => { + const amountToDeposit = utils.parseEther('5'); + const amountToWithdraw = MAX_UINT_AMOUNT; // Still need to figure out why this works :eyes: + + // Preparation + await waitForTx(await weth.deposit({ value: amountToDeposit })); + await waitForTx(await weth.approve(staticAToken.address, amountToDeposit, defaultTxParams)); + + const ctxtInitial = await getContext(ctxtParams); + + // Deposit + await waitForTx( + await staticAToken.deposit(userSigner._address, amountToDeposit, 0, true, defaultTxParams) + ); + + const ctxtAfterDeposit = await getContext(ctxtParams); + // Transfer staticATokens to other user + await waitForTx( + await staticAToken.transfer(user2Signer._address, ctxtAfterDeposit.userStaticATokenBalance) + ); + + const ctxtAfterTransfer = await getContext(ctxtParams); + + // Withdraw + await waitForTx( + await staticAToken + .connect(user2Signer) + .withdraw(user2Signer._address, amountToWithdraw, true, defaultTxParams) + ); + + const ctxtAfterWithdrawal = await getContext(ctxtParams); + + // Claim + await waitForTx(await staticAToken.claimRewards(user2Signer._address)); + const ctxtAfterClaim = await getContext(ctxtParams); + + // TODO: Need to do some checks with the transferred (fresh rewards) as well. + // e.g., we need to show that the received is more than he have gained "by himself" in the same period. + + // Checks + expect(ctxtAfterDeposit.staticATokenATokenBalance).to.be.eq( + ctxtInitial.staticATokenATokenBalance.add(amountToDeposit) + ); + expect(ctxtAfterDeposit.userUnderlyingBalance).to.be.eq( + ctxtInitial.userUnderlyingBalance.sub(amountToDeposit) + ); + expect(ctxtAfterTransfer.user2StaticATokenBalance).to.be.eq( + ctxtAfterDeposit.userStaticATokenBalance + ); + expect(ctxtAfterTransfer.userStaticATokenBalance).to.be.eq(0); + expect(ctxtAfterTransfer.userPendingRewards).to.be.eq(0); + expect(ctxtAfterTransfer.user2PendingRewards).to.be.eq(0); + expect(ctxtAfterWithdrawal.staticATokenSupply).to.be.eq(0); + expect(ctxtAfterWithdrawal.staticATokenATokenBalance).to.be.eq(0); + expect(ctxtAfterWithdrawal.userPendingRewards).to.be.eq(0); + expect(ctxtAfterWithdrawal.user2PendingRewards).to.be.lte( + ctxtAfterWithdrawal.staticATokenStkAaveBalance + ); + expect(ctxtAfterClaim.user2StkAaveBalance).to.be.eq(ctxtAfterWithdrawal.user2PendingRewards); + expect(ctxtAfterClaim.userStkAaveBalance).to.be.eq(0); + expect(ctxtAfterClaim.staticATokenStkAaveBalance).to.be.eq( + ctxtAfterWithdrawal.staticATokenStkAaveBalance.sub(ctxtAfterWithdrawal.user2PendingRewards) + ); + // Expect dust to be left in the contract + expect(ctxtAfterClaim.staticATokenStkAaveBalance).to.be.lt(5); + }); +});