diff --git a/README.md b/README.md index e58ddc5a4..a31a08522 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ To run tests for StETH integration use the following commands: ``` npm install +npm run compile npm run test:steth npm run test:steth-coverage # to run tests with coverage report ``` diff --git a/contracts/mocks/tokens/StETHMocked.sol b/contracts/mocks/tokens/StETHMocked.sol index 0f5326a41..106fbbafc 100644 --- a/contracts/mocks/tokens/StETHMocked.sol +++ b/contracts/mocks/tokens/StETHMocked.sol @@ -9,8 +9,9 @@ pragma solidity 0.6.12; contract StETHMocked { using LidoSafeMath for uint256; - uint256 internal _totalShares; - uint256 internal _pooledEther; + // use there values like real stETH has + uint256 internal _totalShares = 1608965089698263670456320; + uint256 internal _pooledEther = 1701398689820002221426255; mapping(address => uint256) private shares; mapping(address => mapping(address => uint256)) private allowances; @@ -200,7 +201,7 @@ contract StETHMocked { } _mintShares(sender, sharesAmount); - _pooledEther = _pooledEther.add(sharesAmount); + _pooledEther = _pooledEther.add(deposit); return sharesAmount; } diff --git a/contracts/protocol/tokenization/lido/AStETH.sol b/contracts/protocol/tokenization/lido/AStETH.sol index d071592d9..763f7280a 100644 --- a/contracts/protocol/tokenization/lido/AStETH.sol +++ b/contracts/protocol/tokenization/lido/AStETH.sol @@ -105,7 +105,7 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken { uint256 amount, uint256 index ) external override onlyLendingPool { - uint256 amountScaled = amount.rayDiv(_stEthRebasingIndex()).rayDiv(index); + uint256 amountScaled = _toInternalAmount(amount, _stEthRebasingIndex(), index); require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT); _burn(user, amountScaled); @@ -130,7 +130,7 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken { ) external override onlyLendingPool returns (bool) { uint256 previousBalance = super.balanceOf(user); - uint256 amountScaled = amount.rayDiv(_stEthRebasingIndex()).rayDiv(index); + uint256 amountScaled = _toInternalAmount(amount, _stEthRebasingIndex(), index); require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT); _mint(user, amountScaled); @@ -152,10 +152,10 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken { } // Compared to the normal mint, we don't check for rounding errors. - // The amount to mint can easily be very small since it is a fraction of the interest ccrued. + // The amount to mint can easily be very small since it is a fraction of the interest accrued. // In that case, the treasury will experience a (very small) loss, but it - // wont cause potentially valid transactions to fail. - _mint(RESERVE_TREASURY_ADDRESS, amount.rayDiv(_stEthRebasingIndex()).rayDiv(index)); + // won't cause potentially valid transactions to fail. + _mint(RESERVE_TREASURY_ADDRESS, _toInternalAmount(amount, _stEthRebasingIndex(), index)); emit Transfer(address(0), RESERVE_TREASURY_ADDRESS, amount); emit Mint(RESERVE_TREASURY_ADDRESS, amount, index); @@ -207,6 +207,17 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken { return _scaledBalanceOf(user, _stEthRebasingIndex()); } + /** + * @dev Returns the internal balance of the user. The internal balance is the balance of + * the underlying asset of the user (sum of deposits of the user), divided by the current + * liquidity index at the moment of the update and by the current stETH rebasing index. + * @param user The user whose balance is calculated + * @return The internal balance of the user + **/ + function internalBalanceOf(address user) external view returns (uint256) { + return super.balanceOf(user); + } + /** * @dev Returns the scaled balance of the user and the scaled total supply. * @param user The address of the user @@ -247,6 +258,15 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken { return _scaledTotalSupply(_stEthRebasingIndex()); } + /** + * @dev Returns the internal total supply of the token. Represents + * sum(debt/_stEthRebasingIndex/liquidityIndex). + * @return the internal total supply + */ + function internalTotalSupply() external view returns (uint256) { + return super.totalSupply(); + } + /** * @dev Transfers the underlying asset to `target`. Used by the LendingPool to transfer * assets in borrow(), withdraw() and flashLoan() @@ -315,15 +335,17 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken { uint256 amount, bool validate ) internal { - uint256 index = POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS); + uint256 aaveLiquidityIndex = POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS); + uint256 stEthRebasingIndex = _stEthRebasingIndex(); - uint256 rebasingIndex = _stEthRebasingIndex(); - uint256 fromBalanceBefore = _scaledBalanceOf(from, rebasingIndex).rayMul(index); - uint256 toBalanceBefore = _scaledBalanceOf(to, rebasingIndex).rayMul(index); + uint256 fromBalanceBefore = + _scaledBalanceOf(from, stEthRebasingIndex).rayMul(aaveLiquidityIndex); + uint256 toBalanceBefore = _scaledBalanceOf(to, stEthRebasingIndex).rayMul(aaveLiquidityIndex); - super._transfer(from, to, amount.rayDiv(rebasingIndex).rayDiv(index)); + super._transfer(from, to, _toInternalAmount(amount, stEthRebasingIndex, aaveLiquidityIndex)); if (validate) { + require(fromBalanceBefore >= amount, 'ERC20: transfer amount exceeds balance'); POOL.finalizeTransfer( UNDERLYING_ASSET_ADDRESS, from, @@ -334,7 +356,7 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken { ); } - emit BalanceTransfer(from, to, amount, index); + emit BalanceTransfer(from, to, amount, aaveLiquidityIndex); } /** @@ -351,42 +373,33 @@ contract AStETH is VersionedInitializable, IncentivizedERC20, IAToken { _transfer(from, to, amount, true); } - /** - * @return Current rebasin index of stETH in RAY - **/ - function _stEthRebasingIndex() internal view returns (uint256) { - // Below expression returns how much Ether corresponds - // to 10 ** 27 shares. 10 ** 27 was taken to provide - // same precision as AAVE's liquidity index, which - // counted in RAY's (decimals with 27 digits). - return ILido(UNDERLYING_ASSET_ADDRESS).getPooledEthByShares(1e27); - } - function _scaledBalanceOf(address user, uint256 rebasingIndex) internal view returns (uint256) { - return super.balanceOf(user).rayMul(rebasingIndex); + return super.balanceOf(user).mul(rebasingIndex).div(WadRayMath.RAY); } function _scaledTotalSupply(uint256 rebasingIndex) internal view returns (uint256) { - return super.totalSupply().rayMul(rebasingIndex); + return super.totalSupply().mul(rebasingIndex).div(WadRayMath.RAY); } /** - * @dev Returns the internal balance of the user. The internal balance is the balance of - * the underlying asset of the user (sum of deposits of the user), divided by the current - * liquidity index at the moment of the update and by the current stETH rebasing index. - * @param user The user whose balance is calculated - * @return The internal balance of the user + * @return Current rebasing index of stETH in RAY **/ - function internalBalanceOf(address user) external view returns (uint256) { - return super.balanceOf(user); + function _stEthRebasingIndex() internal view returns (uint256) { + // Returns amount of stETH corresponding to 10**27 stETH shares. + // The 10**27 is picked to provide the same precision as the AAVE + // liquidity index, which is in RAY (10**27). + return ILido(UNDERLYING_ASSET_ADDRESS).getPooledEthByShares(WadRayMath.RAY); } /** - * @dev Returns the internal total supply of the token. Represents - * sum(debt/_stEthRebasingIndex/liquidityIndex). - * @return the internal total supply + * @dev Converts amount of astETH to internal shares, based + * on stEthRebasingIndex and aaveLiquidityIndex. */ - function internalTotalSupply() external view returns (uint256) { - return super.totalSupply(); + function _toInternalAmount( + uint256 amount, + uint256 stEthRebasingIndex, + uint256 aaveLiquidityIndex + ) internal view returns (uint256) { + return amount.mul(WadRayMath.RAY).div(stEthRebasingIndex).rayDiv(aaveLiquidityIndex); } } diff --git a/contracts/protocol/tokenization/lido/README.md b/contracts/protocol/tokenization/lido/README.md index 21e787e76..68d7abbc1 100644 --- a/contracts/protocol/tokenization/lido/README.md +++ b/contracts/protocol/tokenization/lido/README.md @@ -57,7 +57,7 @@ function _stEthRebasingIndex() returns (uint256) { // to 10 ** 27 shares. 10 ** 27 was taken to provide // same precision as AAVE's liquidity index, which // counted in RAY's (decimals with 27 digits). - return stETH.getPooledEthByShares(10**27); + return stETH.getPooledEthByShares(WadRayMath.RAY); } ``` @@ -68,27 +68,35 @@ With stETH rebasing index, `AStETH` allows to make rebases profit accountable, a function mint(address user, uint256 amount, uint256 liquidityIndex) { ... uint256 stEthRebasingIndex = _stEthRebasingIndex(); - _mint(user, amount.rayDiv(stEthRebasingIndex).rayDiv(liquidityIndex)); + _mint(user, _toInternalAmount(amount, stEthRebasingIndex, liquidityIndex)); ... } function burn(address user, uint256 amount, uint256 liquidityIndex) { ... uint256 stEthRebasingIndex = _stEthRebasingIndex(); - _burn(user, amount.rayDiv(stEthRebasingIndex)).rayDiv(liquidityIndex); + _burn(user, _toInternalAmount(amount, stEthRebasingIndex, liquidityIndex)); ... } + +function _toInternalAmount( + uint256 amount, + uint256 stEthRebasingIndex, + uint256 aaveLiquidityIndex + ) internal view returns (uint256) { + return amount.mul(WadRayMath.RAY).div(stEthRebasingIndex).rayDiv(aaveLiquidityIndex); + } ``` Then, according to AAVE's definitions, `scaledTotalSupply()` and `scaledBalanceOf()` might be calculated as: ```solidity= function scaledTotalSupply() returns (uint256) { - return _totalSupply.rayMul(_stEthRebasingIndex()); + return _totalSupply.mul(_stEthRebasingIndex()).div(WadRayMath.RAY); } function scaledBalanceOf(address user) returns (uint256) { - return _balances[user].rayMul(_stEthRebasingIndex()); + return _balances[user].mul(_stEthRebasingIndex()).div(WadRayMath.RAY); } ``` diff --git a/test/astETH/__setup.spec.ts b/test/astETH/__setup.spec.ts index acde61950..4227f630e 100644 --- a/test/astETH/__setup.spec.ts +++ b/test/astETH/__setup.spec.ts @@ -6,6 +6,7 @@ import bignumberChai from 'chai-bignumber'; import { solidity } from 'ethereum-waffle'; import { AstEthSetup } from './init'; import '../helpers/utils/math'; +import { wei } from './helpers'; chai.use(bignumberChai()); chai.use(almostEqual()); @@ -15,6 +16,7 @@ let setup: AstEthSetup, evmSnapshotId; before(async () => { setup = await AstEthSetup.deploy(); + await setup.priceFeed.setPrice(wei`0.99 ether`); evmSnapshotId = await hre.ethers.provider.send('evm_snapshot', []); }); diff --git a/test/astETH/asserts.ts b/test/astETH/asserts.ts new file mode 100644 index 000000000..233044fb3 --- /dev/null +++ b/test/astETH/asserts.ts @@ -0,0 +1,88 @@ +import BigNumber from 'bignumber.js'; +import { expect } from 'chai'; +import { toWei } from './helpers'; +import { AstEthSetup, Lender } from './init'; + +export function lt(actual: string, expected: string, message?: string) { + expect(actual).to.be.bignumber.lt(expected, message); +} + +export function gt(actual: string, expected: string, message?: string) { + expect(actual).to.be.bignumber.gt(expected, message); +} + +export function eq(actual: string, expected: string, message?: string) { + expect(actual).is.equal(expected, message); +} + +export function almostEq(actual: string, expected: string, epsilon: string = '1') { + const lowerBound = new BigNumber(expected).minus(epsilon).toString(); + const upperBound = new BigNumber(expected).plus(epsilon).toString(); + expect(actual).to.be.bignumber.lte(upperBound); + expect(actual).to.be.bignumber.gte(lowerBound); +} + +export function lte(actual: string, expected: string, epsilon: string = '1') { + const lowerBound = new BigNumber(expected).minus(epsilon).toString(); + expect(actual).to.be.bignumber.lte(expected); + expect(actual).to.be.bignumber.gte(lowerBound); +} + +export function gte(actual: string, expected: string, epsilon: string = '1') { + const upperBound = new BigNumber(expected).plus(epsilon).toString(); + expect(actual).to.be.bignumber.gte(expected); + expect(actual).to.be.bignumber.lte(upperBound); +} + +export async function astEthBalance( + lender: Lender, + expectedBalance: string, + epsilon: string = '1' +) { + const [balance, internalBalance, liquidityIndex] = await Promise.all([ + lender.astEthBalance(), + lender.astEthInternalBalance(), + lender.lendingPool.getReserveNormalizedIncome(lender.stETH.address).then(toWei), + ]); + lte(balance, expectedBalance, epsilon); + // to validate that amount of shares is correct + // we convert internal balance to stETH shares and assert with astETH balance + const fromInternalBalance = await lender.stETH.getPooledEthByShares(internalBalance).then(toWei); + eq( + new BigNumber(fromInternalBalance).rayMul(new BigNumber(liquidityIndex)).toFixed(0), + balance, + `Unexpected astETH.internalBalanceOf() value` + ); +} + +export async function astEthTotalSupply( + setup: AstEthSetup, + expectedValue: string, + epsilon: string = '1' +) { + const [totalSupply, internalTotalSupply, stEthBalance, liquidityIndex] = await Promise.all([ + setup.astEthTotalSupply(), + setup.astETH.internalTotalSupply().then(toWei), + setup.stETH.balanceOf(setup.astETH.address).then(toWei), + setup.aave.lendingPool.getReserveNormalizedIncome(setup.stETH.address).then(toWei), + ]); + + lte(totalSupply, expectedValue, epsilon); + // to validate that internal number of shares is correct + // internal total supply converts to stETH and assert it with astETH total supply + const fromInternalTotalSupply = await setup.stETH + .getPooledEthByShares(internalTotalSupply) + .then(toWei); + eq( + new BigNumber(fromInternalTotalSupply).rayMul(new BigNumber(liquidityIndex)).toFixed(0), + totalSupply, + `Unexpected astETH.internalTotalSupply()` + ); + eq( + totalSupply, + stEthBalance, + `astETH.totalSupply() is ${totalSupply}, but stETH.balanceOf(astETH) is ${stEthBalance}` + ); +} + +export default { lt, lte, eq, almostEq, gt, gte, astEthBalance, astEthTotalSupply }; diff --git a/test/astETH/astETH-allowance.spec.ts b/test/astETH/astETH-allowance.spec.ts index 9f6a52a28..29de0281b 100644 --- a/test/astETH/astETH-allowance.spec.ts +++ b/test/astETH/astETH-allowance.spec.ts @@ -1,38 +1,45 @@ -import { assertBalance, wei } from './helpers'; +import asserts from './asserts'; +import { wei } from './helpers'; import { setup } from './__setup.spec'; describe('AStETH Allowance:', function () { it('allowance', async () => { const { lenderA, lenderB } = setup.lenders; + const allowanceBefore = await lenderA.astETH.allowance(lenderA.address, lenderB.address); - assertBalance(allowanceBefore.toString(), wei(0)); - await lenderA.astETH.approve(lenderB.address, wei(10)); + asserts.eq(allowanceBefore.toString(), wei`0`); + + await lenderA.astETH.approve(lenderB.address, wei`10 ether`); const allowanceAfter = await lenderA.astETH.allowance(lenderA.address, lenderB.address); - assertBalance(allowanceAfter.toString(), wei(10)); + asserts.eq(allowanceAfter.toString(), wei`10 ether`); }); it('decreaseAllowance', async () => { const { lenderA, lenderB } = setup.lenders; + const allowanceBefore = await lenderA.astETH.allowance(lenderA.address, lenderB.address); - assertBalance(allowanceBefore.toString(), wei(0)); - await lenderA.astETH.approve(lenderB.address, wei(10)); + asserts.eq(allowanceBefore.toString(), wei`0`); + + await lenderA.astETH.approve(lenderB.address, wei`10 ether`); const allowanceAfter = await lenderA.astETH.allowance(lenderA.address, lenderB.address); - assertBalance(allowanceAfter.toString(), wei(10)); + asserts.eq(allowanceAfter.toString(), wei`10 ether`); - await lenderA.astETH.decreaseAllowance(lenderB.address, wei(5)); + await lenderA.astETH.decreaseAllowance(lenderB.address, wei`5 ether`); const allowanceAfterDecrease = await lenderA.astETH.allowance(lenderA.address, lenderB.address); - assertBalance(allowanceAfterDecrease.toString(), wei(5)); + asserts.eq(allowanceAfterDecrease.toString(), wei`5 ether`); }); it('increaseAllowance', async () => { const { lenderA, lenderB } = setup.lenders; + const allowanceBefore = await lenderA.astETH.allowance(lenderA.address, lenderB.address); - assertBalance(allowanceBefore.toString(), wei(0)); - await lenderA.astETH.approve(lenderB.address, wei(10)); + asserts.eq(allowanceBefore.toString(), wei`0`); + + await lenderA.astETH.approve(lenderB.address, wei`10 ether`); const allowanceAfter = await lenderA.astETH.allowance(lenderA.address, lenderB.address); - assertBalance(allowanceAfter.toString(), wei(10)); + asserts.eq(allowanceAfter.toString(), wei`10 ether`); - await lenderA.astETH.increaseAllowance(lenderB.address, wei(5)); + await lenderA.astETH.increaseAllowance(lenderB.address, wei`5 ether`); const allowanceAfterDecrease = await lenderA.astETH.allowance(lenderA.address, lenderB.address); - assertBalance(allowanceAfterDecrease.toString(), wei(15)); + asserts.eq(allowanceAfterDecrease.toString(), wei`15 ether`); }); }); diff --git a/test/astETH/astETH-borrowing.spec.ts b/test/astETH/astETH-borrowing.spec.ts index f992e8bad..f417fee8f 100644 --- a/test/astETH/astETH-borrowing.spec.ts +++ b/test/astETH/astETH-borrowing.spec.ts @@ -1,69 +1,84 @@ import { expect } from 'chai'; import { ProtocolErrors, RateMode } from '../../helpers/types'; -import { wei } from './helpers'; +import asserts from './asserts'; +import { toWei, wei } from './helpers'; import { setup } from './__setup.spec'; describe('AStETH Borrowing', function () { it('VariableDebtStETH total supply is zero', async () => { - expect(await setup.variableDebtStETH.totalSupply().then(wei), '0'); + asserts.eq(await setup.variableDebtStETH.totalSupply().then(toWei), '0'); }); + it('StableDebtStETH total supply is zero', async () => { - expect(await setup.stableDebtStETH.totalSupply().then(wei), '0'); + asserts.eq(await setup.stableDebtStETH.totalSupply().then(toWei), '0'); }); + it('Variable borrowing disabled: must revert with correct message', async () => { const { lenderA, lenderB } = setup.lenders; - await lenderA.depositStEth(wei(10)); + + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); await expect( lenderB.lendingPool.borrow( lenderB.stETH.address, - wei(1), + wei`1 ether`, RateMode.Variable, '0', lenderB.address ) ).to.revertedWith(ProtocolErrors.VL_BORROWING_NOT_ENABLED); }); + it('Stable borrowing disabled: must revert with correct message', async () => { const { lenderA, lenderB } = setup.lenders; - await lenderA.depositStEth(wei(10)); + + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await expect( lenderB.lendingPool.borrow( lenderB.stETH.address, - wei(1), + wei`1 ether`, RateMode.Stable, '0', lenderB.address ) ).to.revertedWith(ProtocolErrors.VL_BORROWING_NOT_ENABLED); }); + it('Variable borrowing enabled: must revert with correct message', async () => { const { lenderA, lenderB } = setup.lenders; await setup.aave.lendingPoolConfigurator.enableBorrowingOnReserve(lenderA.stETH.address, false); - await lenderA.depositStEth(wei(10)); - await lenderB.depositWeth(wei(10)); + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + + await lenderB.depositWeth(wei`10 ether`); await expect( lenderB.lendingPool.borrow( lenderB.stETH.address, - wei(1), + wei`1 ether`, RateMode.Variable, '0', lenderB.address ) ).to.revertedWith('CONTRACT_NOT_ACTIVE'); }); + it('Stable borrowing enabled: must revert with correct message', async () => { const { lenderA, lenderB } = setup.lenders; await setup.aave.lendingPoolConfigurator.enableBorrowingOnReserve(lenderA.stETH.address, true); - await lenderA.depositStEth(wei(10)); - await lenderB.depositWeth(wei(10)); + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + + await lenderB.depositWeth(wei`10 ether`); await expect( lenderB.lendingPool.borrow( lenderB.stETH.address, - wei(1), + wei`1 ether`, RateMode.Stable, '0', lenderB.address diff --git a/test/astETH/astETH-deposits.spec.ts b/test/astETH/astETH-deposits.spec.ts new file mode 100644 index 000000000..521b8f628 --- /dev/null +++ b/test/astETH/astETH-deposits.spec.ts @@ -0,0 +1,67 @@ +import { expect } from 'chai'; +import { zeroAddress } from 'ethereumjs-util'; +import { ProtocolErrors } from '../../helpers/types'; +import asserts from './asserts'; +import { ONE_RAY, toWei, wei } from './helpers'; +import { setup } from './__setup.spec'; + +describe('AStETH Deposits', async () => { + it('First deposit: should mint exact amount of AStETH', async () => { + const { lenderA } = setup.lenders; + const depositAmount = wei`10 ether`; + await lenderA.depositStEth(depositAmount); + + await asserts.astEthBalance(lenderA, depositAmount); + await asserts.astEthTotalSupply(setup, depositAmount); + }); + + it('Multiple deposits: should mint exact amount of AStETH ', async () => { + const { lenderA, lenderB } = setup.lenders; + + // lenderA deposits + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + // lenderB deposits + await lenderB.depositStEth(wei`5 ether`); + await asserts.astEthBalance(lenderB, wei`5 ether`); + await asserts.astEthTotalSupply(setup, wei`15 ether`); + + // lenderB deposits again + await lenderB.depositStEth(wei`7 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthBalance(lenderB, wei`12 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`22 ether`, '3'); + }); + + it('Deposited amount becomes zero after scaling to stETH shares: should revert with correct message', async () => { + const { lenderA } = setup.lenders; + const currentStEthRebasingIndex = await setup.stETH + .getPooledEthByShares(wei`1 ether`) + .then(toWei); + // check that current stEth share price is greater than 1 wei + asserts.gt(currentStEthRebasingIndex, wei`1 ether`); + // when user deposits 1 wei into the pool and stEth share price greater than 1 wei must revert + await expect(lenderA.depositStEth(1)).to.revertedWith(ProtocolErrors.CT_INVALID_MINT_AMOUNT); + }); + + it('Small deposit 2 wei: should mint correct amount of AStETH', async () => { + const { lenderA } = setup.lenders; + + await lenderA.depositStEth('2'); + await asserts.astEthBalance(lenderA, '2'); + await asserts.astEthTotalSupply(setup, '2'); + }); + + it('Deposit Events', async () => { + const { lenderA } = setup.lenders; + + const depositAmount = wei`10 ether`; + await expect(lenderA.depositStEth(depositAmount)) + .to.emit(setup.astETH, 'Transfer') + .withArgs(zeroAddress(), lenderA.address, depositAmount) + .emit(setup.astETH, 'Mint') + .withArgs(lenderA.address, depositAmount, ONE_RAY); + }); +}); diff --git a/test/astETH/astETH-flashloans.spec.ts b/test/astETH/astETH-flashloans.spec.ts index 1e455ab5a..5a61a3ba1 100644 --- a/test/astETH/astETH-flashloans.spec.ts +++ b/test/astETH/astETH-flashloans.spec.ts @@ -1,13 +1,15 @@ +import BigNumber from 'bignumber.js'; import { expect } from 'chai'; import { ProtocolErrors } from '../../helpers/types'; +import asserts from './asserts'; import { advanceTimeAndBlock, - assertBalance, expectedBalanceAfterRebase, expectedBalanceAfterFlashLoan, expectedLiquidityIndexAfterFlashLoan, - ray, wei, + ONE_RAY, + toWei, } from './helpers'; import { setup } from './__setup.spec'; @@ -17,34 +19,42 @@ describe('AStETH FlashLoans', function () { const { lenderA, lenderC } = lenders; // lenderA deposit steth - await lenderA.depositStEth(wei(300)); + await lenderA.depositStEth(wei`300 ether`); + await asserts.astEthBalance(lenderA, wei`300 ether`); + await asserts.astEthTotalSupply(setup, wei`300 ether`); + + let prevAstEthTotalSupply = await setup.astEthTotalSupply(); + let prevLiquidityIndex = ONE_RAY; + let currentLiquidityIndex = ONE_RAY; const reserveDataBeforeFirstFlashLoan = await aave.protocolDataProvider.getReserveData( stETH.address ); - expect(reserveDataBeforeFirstFlashLoan.liquidityIndex.toString()).equals(ray(1)); + asserts.eq(reserveDataBeforeFirstFlashLoan.liquidityIndex.toString(), currentLiquidityIndex); - // lenderC makes flashloan with mode = 1 when liquidity index = 1 - const firstFlashLoanAmount = wei(10); - await lenderC.makeStEthFlashLoanMode0(firstFlashLoanAmount); + // lenderC makes flashloan with mode = 0 when liquidity index = 1 + await lenderC.makeStEthFlashLoanMode0(wei`10 ether`); // Validate that liquidityIndex increased correctly - const reserveDataBeforeSecondFlashLoan = await aave.protocolDataProvider.getReserveData( - stETH.address - ); - expect(reserveDataBeforeSecondFlashLoan.liquidityIndex.toString()).equals( - expectedLiquidityIndexAfterFlashLoan(reserveDataBeforeFirstFlashLoan, firstFlashLoanAmount) - ); - - // lenderB makes another flashloan with mode = 1 when liquidity index != 1 - const secondFlashLoanAmount = wei(20); - await lenderC.makeStEthFlashLoanMode0(secondFlashLoanAmount); - - const reserveDataAfterSecondFlashLoan = await aave.protocolDataProvider.getReserveData( - stETH.address + prevLiquidityIndex = currentLiquidityIndex; + currentLiquidityIndex = await aave.protocolDataProvider + .getReserveData(stETH.address) + .then((rd) => rd.liquidityIndex.toString()); + asserts.eq( + currentLiquidityIndex, + expectedLiquidityIndexAfterFlashLoan(prevLiquidityIndex, prevAstEthTotalSupply, wei`10 ether`) ); - expect(reserveDataAfterSecondFlashLoan.liquidityIndex.toString()).equals( - expectedLiquidityIndexAfterFlashLoan(reserveDataBeforeSecondFlashLoan, secondFlashLoanAmount) + prevAstEthTotalSupply = await setup.astEthTotalSupply(); + + // lenderC makes another flashloan with mode = 0 when liquidity index != 1 + await lenderC.makeStEthFlashLoanMode0(wei`20 ether`); + prevLiquidityIndex = currentLiquidityIndex; + currentLiquidityIndex = await aave.protocolDataProvider + .getReserveData(stETH.address) + .then((rd) => rd.liquidityIndex.toString()); + asserts.eq( + currentLiquidityIndex, + expectedLiquidityIndexAfterFlashLoan(prevLiquidityIndex, prevAstEthTotalSupply, wei`20 ether`) ); }); @@ -52,76 +62,139 @@ describe('AStETH FlashLoans', function () { const { lenderA, lenderB, lenderC } = setup.lenders; // lenderA deposits stETH - const lenderADeposit = wei(25); + const lenderADeposit = wei`25 ether`; await lenderA.depositStEth(lenderADeposit); - let expectedLenderABalance = lenderADeposit; - expect(await lenderA.astEthBalance()).equals(expectedLenderABalance); + await asserts.astEthBalance(lenderA, lenderADeposit); + let expectedLenderABalance = await lenderA.astEthBalance(); - // lenderB makes flash loan - const firstFlashLoanAmount = wei(13); - const providerDataBeforeFirstFlashLoan = await setup.aave.protocolDataProvider.getReserveData( - setup.stETH.address + // validate flashloan receiver has no stETH on balance + await asserts.eq( + await setup.stETH.balanceOf(setup.flashLoanReceiverMock.address).then(toWei), + '0' ); + + // lenderC makes flash loan + const firstFlashLoanAmount = wei`13 ether`; + let prevTotalSupply = await setup.astEthTotalSupply(); await lenderC.makeStEthFlashLoanMode0(firstFlashLoanAmount); // validate reward was distributed correctly expectedLenderABalance = await expectedBalanceAfterFlashLoan( expectedLenderABalance, - providerDataBeforeFirstFlashLoan, + prevTotalSupply, firstFlashLoanAmount ); - expect(await lenderA.astEthBalance()).equals(expectedLenderABalance); + await asserts.astEthBalance(lenderA, expectedLenderABalance); + + // validate that mock receiver might have not more than 1 wei + // after flash loan due to stETH specificities + await asserts.lte( + await setup.stETH.balanceOf(setup.flashLoanReceiverMock.address).then(toWei), + '1' + ); + + // validate that astETH total supply might be only 1 wei less than total supply + // of underlying asset + await asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply() + ); + await asserts.gte(await setup.astEthTotalSupply(), expectedLenderABalance); - // lenderC deposits stETH - const lenderBDeposit = wei(15); + // lenderB deposits stETH + const lenderBDeposit = wei`15 ether`; await lenderB.depositStEth(lenderBDeposit); - let expectedLenderBBalance = lenderBDeposit; - assertBalance(await lenderB.astEthBalance(), expectedLenderBBalance); + let expectedLenderBBalance = await lenderB.astEthBalance(); + await asserts.astEthBalance(lenderB, expectedLenderBBalance, '2'); + await asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply() + ); + await asserts.gte( + await setup.astEthTotalSupply(), + new BigNumber(expectedLenderABalance).plus(expectedLenderBBalance).toFixed(0), + '2' + ); // wait one week await advanceTimeAndBlock(7 * 24 * 3600); // validate balances stays same - assertBalance(await lenderA.astEthBalance(), expectedLenderABalance); - assertBalance(await lenderB.astEthBalance(), expectedLenderBBalance); + await asserts.astEthBalance(lenderA, expectedLenderABalance); + await asserts.astEthBalance(lenderB, expectedLenderBBalance, '2'); + await asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply() + ); + await asserts.gte( + await setup.astEthTotalSupply(), + new BigNumber(expectedLenderABalance).plus(expectedLenderBBalance).toFixed(0), + '2' + ); - // positive 10% rebase happens - await setup.rebaseStETH(+0.1); + // positive 1% rebase happens + await setup.rebaseStETH(+0.01); // validate balances updates correctly - expectedLenderABalance = expectedBalanceAfterRebase(expectedLenderABalance, +0.1); - expectedLenderBBalance = expectedBalanceAfterRebase(expectedLenderBBalance, +0.1); - - assertBalance(await lenderA.astEthBalance(), expectedLenderABalance); - assertBalance(await lenderB.astEthBalance(), expectedLenderBBalance); - - const providerDataBeforeSecondFlashLoan = await setup.aave.protocolDataProvider.getReserveData( - setup.stETH.address + expectedLenderABalance = expectedBalanceAfterRebase(expectedLenderABalance, +0.01); + expectedLenderBBalance = expectedBalanceAfterRebase(expectedLenderBBalance, +0.01); + + await asserts.astEthBalance(lenderA, expectedLenderABalance); + await asserts.astEthBalance(lenderB, expectedLenderBBalance, '2'); + asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply() + ); + await asserts.gte( + await setup.astEthTotalSupply(), + new BigNumber(expectedLenderABalance).plus(expectedLenderBBalance).toFixed(0), + '2' ); + // lenderC makes flashLoan - const secondFlashLoanAmount = wei(13); + const secondFlashLoanAmount = wei`13 ether`; + prevTotalSupply = await setup.astEthTotalSupply(); await lenderC.makeStEthFlashLoanMode0(secondFlashLoanAmount); // validate balances updated correctly expectedLenderABalance = await expectedBalanceAfterFlashLoan( expectedLenderABalance, - providerDataBeforeSecondFlashLoan, + prevTotalSupply, secondFlashLoanAmount ); - expect(await lenderA.astEthBalance()).equals(expectedLenderABalance); + await asserts.astEthBalance(lenderA, expectedLenderABalance); expectedLenderBBalance = expectedBalanceAfterFlashLoan( expectedLenderBBalance, - providerDataBeforeSecondFlashLoan, + prevTotalSupply, secondFlashLoanAmount ); - expect(await lenderB.astEthBalance()).equals(expectedLenderBBalance); + + await asserts.astEthBalance(lenderB, expectedLenderBBalance, '2'); + asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply() + ); + await asserts.gte( + await setup.astEthTotalSupply(), + new BigNumber(expectedLenderABalance).plus(expectedLenderBBalance).toFixed(0), + '2' + ); // wait one week await advanceTimeAndBlock(30 * 24 * 3060); // validate balances - assertBalance(await lenderA.astEthBalance(), expectedLenderABalance); - assertBalance(await lenderB.astEthBalance(), expectedLenderBBalance); + await asserts.astEthBalance(lenderA, expectedLenderABalance); + await asserts.astEthBalance(lenderB, expectedLenderBBalance, '2'); + asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply() + ); + await asserts.gte( + await setup.astEthTotalSupply(), + new BigNumber(expectedLenderABalance).plus(expectedLenderBBalance).toFixed(0), + '2' + ); // negative rebase -5 % happens await setup.rebaseStETH(-0.05); @@ -129,28 +202,56 @@ describe('AStETH FlashLoans', function () { // validate balances expectedLenderABalance = expectedBalanceAfterRebase(expectedLenderABalance, -0.05); expectedLenderBBalance = expectedBalanceAfterRebase(expectedLenderBBalance, -0.05); - assertBalance(await lenderA.astEthBalance(), expectedLenderABalance); - assertBalance(await lenderB.astEthBalance(), expectedLenderBBalance); + + await asserts.astEthBalance(lenderA, expectedLenderABalance); + await asserts.astEthBalance(lenderB, expectedLenderBBalance, '2'); + asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply(), + '2' + ); + await asserts.gte( + await setup.astEthTotalSupply(), + new BigNumber(expectedLenderABalance).plus(expectedLenderBBalance).toFixed(0), + '2' + ); // lenderA withdraws all his tokens - await lenderA.withdrawStEth(expectedLenderABalance); - assertBalance(await lenderA.astEthBalance(), wei(0)); - assertBalance(await lenderB.astEthBalance(), expectedLenderBBalance); - - // lenderC withdraws all his tokens - await lenderB.withdrawStEth(expectedLenderBBalance); - assertBalance(await lenderA.astEthBalance(), wei(0)); - assertBalance(await lenderB.astEthBalance(), wei(0)); + await lenderA.withdrawStEth(await lenderA.astEthBalance()); + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthBalance(lenderB, expectedLenderBBalance, '2'); + asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply(), + '2' + ); + await asserts.gte( + await setup.astEthTotalSupply(), + new BigNumber(expectedLenderBBalance).toFixed(0), + '2' + ); + + // lenderB withdraws all his tokens + await lenderB.withdrawStEth(await lenderB.astEthBalance()); + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthBalance(lenderB, '1'); + + await asserts.almostEq( + await setup.stETH.balanceOf(setup.astETH.address).then(toWei), + await setup.astEthTotalSupply(), + '2' + ); + await asserts.gte(await setup.astEthTotalSupply(), '0', '2'); }); it('Flash Loan with mode = 1 when stable rate borrowing disabled must revert with VL_BORROWING_NOT_ENABLED', async () => { const { lenderA, lenderC } = setup.lenders; // lenderA deposit steth - await lenderA.depositStEth(wei(300)); + await lenderA.depositStEth(wei`300 ether`); // lenderC makes flashloan with mode = 1 when liquidity index = 1 - await expect(lenderC.makeStEthFlashLoanMode1(wei(10))).to.revertedWith( + await expect(lenderC.makeStEthFlashLoanMode1(wei`10 ether`)).to.revertedWith( ProtocolErrors.VL_BORROWING_NOT_ENABLED ); }); @@ -159,10 +260,10 @@ describe('AStETH FlashLoans', function () { const { lenderA, lenderC } = setup.lenders; // lenderA deposit steth - await lenderA.depositStEth(wei(300)); + await lenderA.depositStEth(wei`300 ether`); - // lenderC makes flashloan with mode = 1 when liquidity index = 1 - await expect(lenderC.makeStEthFlashLoanMode2(wei(10))).to.revertedWith( + // lenderC makes flashloan with mode = 2 when liquidity index = 1 + await expect(lenderC.makeStEthFlashLoanMode2(wei`10 ether`)).to.revertedWith( ProtocolErrors.VL_BORROWING_NOT_ENABLED ); }); @@ -172,10 +273,12 @@ describe('AStETH FlashLoans', function () { const { lenderA, lenderC } = setup.lenders; // lenderA deposit steth - await lenderA.depositStEth(wei(300)); + await lenderA.depositStEth(wei`300 ether`); // lenderC makes flashloan with mode = 1 when liquidity index = 1 - await expect(lenderC.makeStEthFlashLoanMode1(wei(10))).to.revertedWith('CONTRACT_NOT_ACTIVE'); + await expect(lenderC.makeStEthFlashLoanMode1(wei`10 ether`)).to.revertedWith( + 'CONTRACT_NOT_ACTIVE' + ); }); it('Flash Loan with mode = 2 when variable rate borrowing enabled must revert with CONTRACT_NOT_ACTIVE', async () => { @@ -183,9 +286,11 @@ describe('AStETH FlashLoans', function () { const { lenderA, lenderC } = setup.lenders; // lenderA deposit steth - await lenderA.depositStEth(wei(300)); + await lenderA.depositStEth(wei`300 ether`); - // lenderB makes flashloan with mode = 1 when liquidity index = 1 - await expect(lenderC.makeStEthFlashLoanMode2(wei(10))).to.revertedWith('CONTRACT_NOT_ACTIVE'); + // lenderB makes flashloan with mode = 2 when liquidity index = 1 + await expect(lenderC.makeStEthFlashLoanMode2(wei`10 ether`)).to.revertedWith( + 'CONTRACT_NOT_ACTIVE' + ); }); }); diff --git a/test/astETH/astETH-happy-path.spec.ts b/test/astETH/astETH-happy-path.spec.ts index d49df25f9..290f94eed 100644 --- a/test/astETH/astETH-happy-path.spec.ts +++ b/test/astETH/astETH-happy-path.spec.ts @@ -1,134 +1,147 @@ +import BigNumber from 'bignumber.js'; import { MAX_UINT_AMOUNT } from '../../helpers/constants'; -import { assertBalance, wei, advanceTimeAndBlock } from './helpers'; +import asserts from './asserts'; +import { advanceTimeAndBlock, wei } from './helpers'; import { setup } from './__setup.spec'; describe('AStETH Happy Path', function () { it('Should be passed without exceptions', async () => { - const { aave, stETH, astETH } = setup; const { lenderA, lenderB, lenderC } = setup.lenders; // lender A deposits 100 stETH - await lenderA.depositStEth(wei(100)); + await lenderA.depositStEth(wei`100 ether`); - assertBalance(await astETH.totalSupply().then(wei), wei(100)); - assertBalance(await lenderA.astEthBalance(), wei(100)); - assertBalance(await lenderB.astEthBalance(), wei(0)); - assertBalance(await lenderC.astEthBalance(), wei(0)); + await asserts.astEthBalance(lenderA, wei`100 ether`); + await asserts.astEthBalance(lenderB, wei`0 ether`); + await asserts.astEthBalance(lenderC, wei`0 ether`); + await asserts.astEthTotalSupply(setup, wei`100 ether`); // wait one month await advanceTimeAndBlock(30 * 24 * 3600); // validate that balance stays same - assertBalance(await astETH.totalSupply().then(wei), wei(100)); - assertBalance(await lenderA.astEthBalance(), wei(100)); - assertBalance(await lenderB.astEthBalance(), wei(0)); - assertBalance(await lenderC.astEthBalance(), wei(0)); + await asserts.astEthBalance(lenderA, wei`100 ether`); + await asserts.astEthBalance(lenderB, wei`0 ether`); + await asserts.astEthBalance(lenderC, wei`0 ether`); + await asserts.astEthTotalSupply(setup, wei`100 ether`); // lender B deposits 50 stETH - await lenderB.depositStEth(wei(50)); + await lenderB.depositStEth(wei`50 ether`); - assertBalance(await astETH.totalSupply().then(wei), wei(150)); - assertBalance(await lenderA.astEthBalance(), wei(100)); - assertBalance(await lenderB.astEthBalance(), wei(50)); - assertBalance(await lenderC.astEthBalance(), wei(0)); + await asserts.astEthBalance(lenderA, wei`100 ether`); + await asserts.astEthBalance(lenderB, wei`50 ether`); + await asserts.astEthBalance(lenderC, wei`0 ether`); + await asserts.astEthTotalSupply(setup, wei`150 ether`, '2'); - // positive rebase 1% - await setup.rebaseStETH(0.01); + // positive rebase 0.1% + await setup.rebaseStETH(0.001); - assertBalance(await astETH.totalSupply().then(wei), wei(151.5)); - assertBalance(await lenderA.astEthBalance(), wei(101)); - assertBalance(await lenderB.astEthBalance(), wei(50.5)); - assertBalance(await lenderC.astEthBalance(), wei(0)); + await asserts.astEthBalance(lenderA, wei`100.1 ether`); + await asserts.astEthBalance(lenderB, wei`50.05 ether`); + await asserts.astEthBalance(lenderC, wei`0`); + await asserts.astEthTotalSupply(setup, wei`150.15 ether`, '2'); // wait 1 month await advanceTimeAndBlock(3600 * 24 * 30); // validate balances stays same - assertBalance(await astETH.totalSupply().then(wei), wei(151.5)); - assertBalance(await lenderA.astEthBalance(), wei(101)); - assertBalance(await lenderB.astEthBalance(), wei(50.5)); - assertBalance(await lenderC.astEthBalance(), wei(0)); + await asserts.astEthBalance(lenderA, wei`100.1 ether`); + await asserts.astEthBalance(lenderB, wei`50.05 ether`); + await asserts.astEthBalance(lenderC, wei`0`); + await asserts.astEthTotalSupply(setup, wei`150.15 ether`, '2'); // lender C deposits 50 stETH - await lenderC.depositStEth(wei(50)); + await lenderC.depositStEth(wei`50 ether`); - assertBalance(await astETH.totalSupply().then(wei), wei(201.5)); - assertBalance(await lenderA.astEthBalance(), wei(101)); - assertBalance(await lenderB.astEthBalance(), wei(50.5)); - assertBalance(await lenderC.astEthBalance(), wei(50)); + await asserts.astEthBalance(lenderA, wei`100.1 ether`); + await asserts.astEthBalance(lenderB, wei`50.05 ether`); + await asserts.astEthBalance(lenderC, wei`50 ether`); + await asserts.astEthTotalSupply(setup, wei`200.15 ether`, '2'); // wait 1 month await advanceTimeAndBlock(3600 * 24 * 30); // validate balances stays same - assertBalance(await astETH.totalSupply().then(wei), wei(201.5)); - assertBalance(await lenderA.astEthBalance(), wei(101)); - assertBalance(await lenderB.astEthBalance(), wei(50.5)); - assertBalance(await lenderC.astEthBalance(), wei(50)); + await asserts.astEthBalance(lenderA, wei`100.1 ether`); + await asserts.astEthBalance(lenderB, wei`50.05 ether`); + await asserts.astEthBalance(lenderC, wei`50 ether`); + await asserts.astEthTotalSupply(setup, wei`200.15 ether`, '2'); - // negative rebase happens -0.05% + // negative rebase happens -5% await setup.rebaseStETH(-0.05); - assertBalance(await astETH.totalSupply().then(wei), wei(191.425)); - assertBalance(await lenderA.astEthBalance(), wei(95.95)); - assertBalance(await lenderB.astEthBalance(), wei(47.975)); - assertBalance(await lenderC.astEthBalance(), wei(47.5)); + await asserts.astEthBalance(lenderA, wei`95.095 ether`); + await asserts.astEthBalance(lenderB, wei`47.5475 ether`); + await asserts.astEthBalance(lenderC, wei`47.5 ether`); + await asserts.astEthTotalSupply(setup, wei`190.1425 ether`, '2'); // lender A transfers 50 astETH to lender C - await lenderA.transferAstEth(lenderC.address, wei(50)); + await lenderA.transferAstEth(lenderC.address, wei`50 ether`); - assertBalance(await astETH.totalSupply().then(wei), wei(191.425)); - assertBalance(await lenderA.astEthBalance(), wei(45.95)); - assertBalance(await lenderB.astEthBalance(), wei(47.975)); - assertBalance(await lenderC.astEthBalance(), wei(97.5)); + await asserts.astEthBalance(lenderA, wei`45.095 ether`); + await asserts.astEthBalance(lenderB, wei`47.5475 ether`); + await asserts.astEthBalance(lenderC, wei`97.5 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`190.1425 ether`, '2'); // lenderA withdraws 30 stETH from the pool - await lenderA.withdrawStEth(wei(30)); - - assertBalance(await astETH.totalSupply().then(wei), wei(161.425)); - assertBalance(await lenderA.astEthBalance(), wei(15.95)); - assertBalance(await lenderB.astEthBalance(), wei(47.975)); - assertBalance(await lenderC.astEthBalance(), wei(97.5)); + let lenderAAstEthBalanceBefore = await lenderA.astEthBalance(); + let lenderAStEthBalanceBefore = await lenderA.stEthBalance(); + await lenderA.withdrawStEth(wei`30 ether`); + + // Validate that after withdrawal user will receive the same + // amount of stETH how much astETH was burned. Due to stETH rebasing + // lender still may have one wei of astETH on balance. + asserts.eq( + new BigNumber(lenderAAstEthBalanceBefore).minus(await lenderA.astEthBalance()).toString(), + new BigNumber(await lenderA.stEthBalance()).minus(lenderAStEthBalanceBefore).toString() + ); + + // it's possible that after withdraw user will have one wei on balance + // count it in assertion + const expectedLenderABalanceAfterWithdraw = new BigNumber(wei`15.095 ether`).plus(1).toFixed(0); + await asserts.astEthBalance(lenderA, expectedLenderABalanceAfterWithdraw); + await asserts.astEthBalance(lenderB, wei`47.5475 ether`); + await asserts.astEthBalance(lenderC, wei`97.5 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`160.1425 ether`, '2'); // wait 1 month await advanceTimeAndBlock(3600 * 24 * 30); // validate balances stays same - assertBalance(await astETH.totalSupply().then(wei), wei(161.425)); - assertBalance(await lenderA.astEthBalance(), wei(15.95)); - assertBalance(await lenderB.astEthBalance(), wei(47.975)); - assertBalance(await lenderC.astEthBalance(), wei(97.5)); + await asserts.astEthBalance(lenderA, expectedLenderABalanceAfterWithdraw); + await asserts.astEthBalance(lenderB, wei`47.5475 ether`); + await asserts.astEthBalance(lenderC, wei`97.5 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`160.1425 ether`, '2'); // lender A withdraws all his tokens - await lenderA.withdrawStEth(wei(15.95)); - - assertBalance(await astETH.totalSupply().then(wei), wei(145.475)); - assertBalance(await lenderA.astEthBalance(), wei(0)); - assertBalance(await lenderB.astEthBalance(), wei(47.975)); - assertBalance(await lenderC.astEthBalance(), wei(97.5)); + await lenderA.withdrawStEth(await lenderA.astEthBalance()); - // positive rebase happens +7% - await setup.rebaseStETH(0.07); + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthBalance(lenderB, wei`47.5475 ether`); + await asserts.astEthBalance(lenderC, wei`97.5 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`145.0475 ether`, '2'); - assertBalance(await astETH.totalSupply().then(wei), wei(155.65825)); - assertBalance(await lenderA.astEthBalance(), wei(0)); - assertBalance(await lenderB.astEthBalance(), wei(51.33325)); - assertBalance(await lenderC.astEthBalance(), wei(104.325)); + // positive rebase happens +5.3% + await setup.rebaseStETH(0.053); + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthBalance(lenderB, wei`50.0675175 ether`); + await asserts.astEthBalance(lenderC, wei`102.6675 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`152.7350175 ether`, '2'); // lender B withdraws all his tokens await lenderB.withdrawStEth(MAX_UINT_AMOUNT); - assertBalance(await astETH.totalSupply().then(wei), wei(104.325)); - assertBalance(await lenderA.astEthBalance(), wei(0)); - assertBalance(await lenderB.astEthBalance(), wei(0)); - assertBalance(await lenderC.astEthBalance(), wei(104.325)); + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthBalance(lenderB, '1'); + await asserts.astEthBalance(lenderC, wei`102.6675 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`102.6675 ether`, '2'); // lender C withdraws all his tokens await lenderC.withdrawStEth(MAX_UINT_AMOUNT); - assertBalance(await astETH.totalSupply().then(wei), wei(0)); - assertBalance(await lenderA.astEthBalance(), wei(0)); - assertBalance(await lenderB.astEthBalance(), wei(0)); - assertBalance(await lenderC.astEthBalance(), wei(0)); + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthBalance(lenderB, '1'); + await asserts.astEthBalance(lenderC, '1'); + await asserts.astEthTotalSupply(setup, '3'); }); }); diff --git a/test/astETH/astETH-liquidations.spec.ts b/test/astETH/astETH-liquidations.spec.ts index f4e32d698..16c4cc4b0 100644 --- a/test/astETH/astETH-liquidations.spec.ts +++ b/test/astETH/astETH-liquidations.spec.ts @@ -1,202 +1,312 @@ import { _TypedDataEncoder } from '@ethersproject/hash'; import BigNumber from 'bignumber.js'; -import { expect } from 'chai'; import { MAX_UINT_AMOUNT } from '../../helpers/constants'; -import { RateMode } from '../../helpers/types'; -import { assertBalance, wei } from './helpers'; +import { strategySTETH } from '../../markets/aave/reservesConfigs'; +import asserts from './asserts'; +import { wei } from './helpers'; import { setup } from './__setup.spec'; const EPSILON = '100000000000'; +const HALF_EPSILON = '50000000000'; +const LIQUIDATION_BONUS = new BigNumber(strategySTETH.liquidationBonus); describe('AStETH Liquidation', function () { - it('liquidation negative rebase + stable debt', async () => { + it('liquidation on negative rebase + stable debt', async () => { const { stETH, weth, lenders, aave } = setup; const borrower = lenders.lenderA; const liquidator = lenders.lenderB; - await borrower.depositStEth(wei(7.5)); - await borrower.lendingPool.borrow(weth.address, wei(5), RateMode.Stable, '0', borrower.address); + // borrower deposits stETH to use as collateral + await borrower.depositStEth(wei`7.5 ether`); + await asserts.astEthBalance(borrower, wei`7.5 ether`); + + const borrowAmount = wei`5 ether`; + await borrower.borrowWethStable(borrowAmount); + + // validate that health factor is above 1 const userGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); - expect(userGlobalData.healthFactor.toString()).to.be.bignumber.gt( - wei(1), - 'Health Factor Below 1' - ); - await setup.rebaseStETH(-0.12); // price drop 12% + asserts.gt(userGlobalData.healthFactor.toString(), wei`1 ether`, 'Health Factor Below 1'); + + // negative rebase happens + await setup.rebaseStETH(-0.12); // negative rebase 12% const userGlobalDataAfterRebase = await borrower.lendingPool.getUserAccountData( borrower.address ); - expect(userGlobalDataAfterRebase.healthFactor.toString()).to.be.bignumber.lt( - wei(1), - 'Health Factor Below 1' + + // validate that after negative rebase health factor became below 1 + asserts.lt( + userGlobalDataAfterRebase.healthFactor.toString(), + wei`1 ether`, + 'Health Factor Above 1' ); - await liquidator.weth.deposit({ value: wei(10) }); - await liquidator.weth.approve(liquidator.lendingPool.address, wei(10)); + // validate liquidator had no astETH before liquidation + await asserts.astEthBalance(liquidator, '0'); + + // liquidator deposits 10 weth to make liquidation + const liquidatorWethBalance = wei`10 ether`; + await liquidator.weth.deposit({ value: liquidatorWethBalance }); + + // set allowance for lending pool to withdraw WETH from liquidator + const liquidationAmount = wei`1 ether`; + await liquidator.weth.approve(liquidator.lendingPool.address, liquidationAmount); + + // liquidator liquidates 1 ether of debt of the borrower await liquidator.lendingPool.liquidationCall( stETH.address, weth.address, borrower.address, - wei(1), + liquidationAmount, true ); - const liquidatorWethBalance = await liquidator.wethBalance(); - const liquidatorAstEthBalance = await liquidator.astEthBalance(); - assertBalance(liquidatorWethBalance, wei(9)); - assertBalance(liquidatorAstEthBalance, wei(1.075)); + // validate that was withdrawn correct amount of WETH from liquidator + asserts.eq( + await liquidator.wethBalance(), + new BigNumber(liquidatorWethBalance).minus(liquidationAmount).toString() + ); - const userReserveDataAfterLiquidation = await aave.protocolDataProvider.getUserReserveData( - weth.address, - borrower.address + // validate that liquidator received correct amount of astETH + const price = await aave.priceOracle.getAssetPrice(setup.stETH.address); + await asserts.astEthBalance( + liquidator, + new BigNumber(liquidationAmount) + .percentMul(LIQUIDATION_BONUS) + .multipliedBy(wei`1 ether`) + .dividedBy(price.toString()) + .toFixed(0, 1) ); - const userGlobalDataAfterLiquidation = await borrower.lendingPool.getUserAccountData( - borrower.address + + const [{ healthFactor }, { currentStableDebt }] = await Promise.all([ + borrower.lendingPool.getUserAccountData(borrower.address), + aave.protocolDataProvider.getUserReserveData(weth.address, borrower.address), + ]); + // validate that health factor of borrower recovered + asserts.gt(healthFactor.toString(), wei`1 ether`); + // validate that were burned correct amount of debt tokens + asserts.gte( + currentStableDebt.toString(), + new BigNumber(borrowAmount).minus(liquidationAmount).toString(), + EPSILON ); - expect(userGlobalDataAfterLiquidation.healthFactor.toString()).to.be.bignumber.gt(wei(1)); - assertBalance(userReserveDataAfterLiquidation.currentStableDebt.toString(), wei(4), EPSILON); }); - it('liquidation negative rebase below strategy assumption: health factor must fall', async () => { + + it('liquidation on negative rebase + variable debt', async () => { const { stETH, weth, lenders, aave } = setup; const borrower = lenders.lenderA; const liquidator = lenders.lenderB; - await borrower.depositStEth(wei(7.5)); - await borrower.lendingPool.borrow( - weth.address, - wei(5), - RateMode.Variable, - '0', - borrower.address - ); + // borrower deposits stETH to use as collateral + await borrower.depositStEth(wei`7.5 ether`); + await asserts.astEthBalance(borrower, wei`7.5 ether`); + + const borrowAmount = wei`5 ether`; + await borrower.borrowWethVariable(borrowAmount); + + // validate that health factor is above 1 const userGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); - expect(userGlobalData.healthFactor.toString()).to.be.bignumber.gt( - wei(1), - 'Health Factor Below 1' - ); - await setup.rebaseStETH(-0.3); // price drop 30% + asserts.gt(userGlobalData.healthFactor.toString(), wei`1 ether`); + + // negative rebase happens + await setup.rebaseStETH(-0.12); // negative rebase 12% const userGlobalDataAfterRebase = await borrower.lendingPool.getUserAccountData( borrower.address ); - expect(userGlobalDataAfterRebase.healthFactor.toString()).to.be.bignumber.lt( - wei(1), - 'Health Factor Below 1' - ); - await liquidator.weth.deposit({ value: wei(10) }); - await liquidator.weth.approve(liquidator.lendingPool.address, wei(10)); + // validate that after negative rebase health factor became below 1 + asserts.lt(userGlobalDataAfterRebase.healthFactor.toString(), wei`1 ether`); + + // validate liquidator had no astETH before liquidation + await asserts.astEthBalance(liquidator, '0'); + + // liquidator deposits 10 weth to make liquidation + const liquidatorWethBalance = wei`10 ether`; + await liquidator.weth.deposit({ value: liquidatorWethBalance }); + + // set allowance for lending pool to withdraw WETH from liquidator + const liquidationAmount = wei`1 ether`; + await liquidator.weth.approve(liquidator.lendingPool.address, liquidationAmount); + + // liquidator liquidates 1 ether of debt of the borrower await liquidator.lendingPool.liquidationCall( stETH.address, weth.address, borrower.address, - MAX_UINT_AMOUNT, + liquidationAmount, true ); - const liquidatorWethBalance = await liquidator.wethBalance(); - const liquidatorAstEthBalance = await liquidator.astEthBalance(); - assertBalance(liquidatorWethBalance, wei(7.5), EPSILON); - assertBalance(liquidatorAstEthBalance, wei(2.6875), EPSILON); - - const userReserveDataAfterLiquidation = await aave.protocolDataProvider.getUserReserveData( - weth.address, - borrower.address - ); - const userGlobalDataAfterLiquidation = await borrower.lendingPool.getUserAccountData( - borrower.address + // validate that was withdrawn correct amount of WETH from liquidator + asserts.eq( + await liquidator.wethBalance(), + new BigNumber(liquidatorWethBalance).minus(liquidationAmount).toString() ); - expect(userGlobalDataAfterLiquidation.healthFactor.toString()).to.be.bignumber.lt( - userGlobalDataAfterRebase.healthFactor.toString() + // validate that liquidator received correct amount of astETH + const price = await aave.priceOracle.getAssetPrice(setup.stETH.address); + await asserts.astEthBalance( + liquidator, + new BigNumber(liquidationAmount) + .percentMul(LIQUIDATION_BONUS) + .multipliedBy(wei`1 ether`) + .dividedBy(price.toString()) + .toFixed(0, 1) ); - assertBalance( - userReserveDataAfterLiquidation.currentVariableDebt.toString(), - wei(2.5), + + const [{ healthFactor }, { currentVariableDebt }] = await Promise.all([ + borrower.lendingPool.getUserAccountData(borrower.address), + aave.protocolDataProvider.getUserReserveData(weth.address, borrower.address), + ]); + + // validate that health factor of borrower recovered + asserts.gt(healthFactor.toString(), wei`1 ether`); + + // validate that were burned correct amount of debt tokens + + asserts.gte( + currentVariableDebt.toString(), + new BigNumber(borrowAmount).minus(liquidationAmount).toString(), EPSILON ); }); - it('liquidation on negative rebase', async () => { - const { stETH, weth, lenders, aave } = setup; + + it('liquidation on price drop + variable debt', async () => { + const { stETH, weth, lenders, aave, priceFeed } = setup; const borrower = lenders.lenderA; const liquidator = lenders.lenderB; - await borrower.depositStEth(wei(7.5)); - await borrower.lendingPool.borrow( - weth.address, - wei(5), - RateMode.Variable, - '0', - borrower.address - ); + // borrower deposits stETH to use as collateral + await borrower.depositStEth(wei`8 ether`); + await asserts.astEthBalance(borrower, wei`8 ether`); + + const borrowAmount = wei`5 ether`; + await borrower.borrowWethVariable(borrowAmount); + + // validate that health factor is above 1 const userGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); - expect(userGlobalData.healthFactor.toString()).to.be.bignumber.gt( - wei(1), - 'Health Factor Below 1' - ); - await setup.rebaseStETH(-0.12); // price drop 12% + asserts.gt(userGlobalData.healthFactor.toString(), wei`1 ether`); + + // stETH price drop happens + await priceFeed.setPrice(wei`0.8 ether`); // price drop 20 % const userGlobalDataAfterRebase = await borrower.lendingPool.getUserAccountData( borrower.address ); - expect(userGlobalDataAfterRebase.healthFactor.toString()).to.be.bignumber.lt( - wei(1), - 'Health Factor Below 1' + + // validate that after negative rebase health factor became below 1 + asserts.lt(userGlobalDataAfterRebase.healthFactor.toString(), wei`1 ether`); + + // validate liquidator had no astETH before liquidation + await asserts.astEthBalance(liquidator, '0'); + + // liquidator deposits 10 weth to make liquidation + const liquidatorWethBalance = wei`10 ether`; + await liquidator.weth.deposit({ value: liquidatorWethBalance }); + + // set allowance for lending pool to withdraw WETH from liquidator + const expectedLiquidationAmount = new BigNumber(borrowAmount).div(2).toFixed(0); + await liquidator.weth.approve( + liquidator.lendingPool.address, + new BigNumber(expectedLiquidationAmount).plus(EPSILON).toFixed() ); - await liquidator.weth.deposit({ value: wei(10) }); - await liquidator.weth.approve(liquidator.lendingPool.address, wei(10)); + // liquidator liquidates max allowed amount of debt (50%) of the borrower await liquidator.lendingPool.liquidationCall( stETH.address, weth.address, borrower.address, - wei(1), + MAX_UINT_AMOUNT, true ); - const liquidatorWethBalance = await liquidator.wethBalance(); - const liquidatorAstEthBalance = await liquidator.astEthBalance(); - assertBalance(liquidatorWethBalance, wei(9)); - assertBalance(liquidatorAstEthBalance, wei(1.075)); + // validate that was withdrawn correct amount of WETH from liquidator + asserts.lte( + await liquidator.wethBalance(), + new BigNumber(liquidatorWethBalance).minus(expectedLiquidationAmount).toFixed(), + EPSILON + ); - const userReserveDataAfterLiquidation = await aave.protocolDataProvider.getUserReserveData( - weth.address, - borrower.address + // validate that liquidator received correct amount of astETH + const price = await aave.priceOracle.getAssetPrice(setup.stETH.address); + await asserts.astEthBalance( + liquidator, + new BigNumber(expectedLiquidationAmount) + .percentMul(LIQUIDATION_BONUS) + .multipliedBy(wei`1 ether`) + .dividedBy(price.toString()) + .plus(HALF_EPSILON) + .toFixed(0, 1), + HALF_EPSILON ); - const userGlobalDataAfterLiquidation = await borrower.lendingPool.getUserAccountData( - borrower.address + + const [{ healthFactor }, { currentVariableDebt }] = await Promise.all([ + borrower.lendingPool.getUserAccountData(borrower.address), + aave.protocolDataProvider.getUserReserveData(weth.address, borrower.address), + ]); + + // validate that health factor of borrower recovered + asserts.gt(healthFactor.toString(), wei`1 ether`); + + // validate that were burned correct amount of debt tokens + asserts.gte( + currentVariableDebt.toString(), + new BigNumber(borrowAmount).minus(expectedLiquidationAmount).toString(), + EPSILON ); - expect(userGlobalDataAfterLiquidation.healthFactor.toString()).to.be.bignumber.gt(wei(1)); - assertBalance(userReserveDataAfterLiquidation.currentVariableDebt.toString(), wei(4), EPSILON); }); - it('liquidation on price drop', async () => { - const { stETH, weth, lenders, aave, priceFeed } = setup; - const borrower = lenders.lenderA; - const liquidator = lenders.lenderB; + it('Liquidate all astETH collateral', async () => { + const { stETH, weth, lenders, priceFeed, aave } = setup; + const borrower = lenders.lenderB; + const liquidator = lenders.lenderC; - await borrower.depositStEth(wei(8)); - await borrower.lendingPool.borrow( - weth.address, - wei(5), - RateMode.Variable, - '0', - borrower.address - ); + // borrower deposits stETH to use as collateral + await borrower.depositStEth(wei`20 ether`); + await asserts.astEthBalance(borrower, wei`20 ether`); - const userGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); - expect(userGlobalData.healthFactor.toString()).to.be.bignumber.gt( - wei(1), - 'Health Factor Below 1' - ); - await priceFeed.setPrice(wei(0.8)); // price drop 8% - const userGlobalDataAfterRebase = await borrower.lendingPool.getUserAccountData( - borrower.address - ); - expect(userGlobalDataAfterRebase.healthFactor.toString()).to.be.bignumber.lt( - wei(1), - 'Health Factor Below 1' + const borrowAmount = wei`10 ether`; + await borrower.borrowWethVariable(borrowAmount); + + // validate that health factor is above 1 + let borrowerGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); + asserts.gt(borrowerGlobalData.healthFactor.toString(), wei`1 ether`); + + // negative rebase happens (75%) + await setup.rebaseStETH(-0.75); + borrowerGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); + + // validate that after negative rebase health factor became below 1 + asserts.lt(borrowerGlobalData.healthFactor.toString(), wei`1 ether`); + + // validate that borrower astETH balance becomes almost equal (might be 1 wei less) + // to max allowed liquidation amount + const maxAllowedLiquidationAmount = new BigNumber(borrowAmount).div(2).toFixed(0); + await asserts.astEthBalance(borrower, maxAllowedLiquidationAmount); + + // validate liquidator had no astETH before liquidation + await asserts.astEthBalance(liquidator, '0'); + + // liquidator deposits 8 weth to make liquidation + const liquidatorWethBalance = wei`8 ether`; + await liquidator.weth.deposit({ value: liquidatorWethBalance }); + + // set allowance for lending pool to withdraw WETH from liquidator + await liquidator.weth.approve( + liquidator.lendingPool.address, + new BigNumber(maxAllowedLiquidationAmount).plus(EPSILON).toFixed() ); - await liquidator.weth.deposit({ value: wei(3) }); - await liquidator.weth.approve(liquidator.lendingPool.address, wei(3)); + // In the current test case, astETH balance of borrower equal (or less on 1 wei) to borrowAmount / 2 + // actual debt of user is borrowAmount ether in WETH. Max theoretical amount of weth liquidator might + // compensate 50 % of borrow (borrowAmount / 2), but in practice, liquidation can't be greater than + // liquidator.astEthBalance() * stEthPrice / 10 ** 18 / LIQUIDATION_BONUS. + const price = await aave.priceOracle.getAssetPrice(setup.stETH.address); + const expectedLiquidationAmount = new BigNumber(maxAllowedLiquidationAmount) + .multipliedBy(price.toString()) + .dividedBy(wei`1 ether`) + .percentDiv(LIQUIDATION_BONUS); + + // liquidator liquidates max allowed amount of debt (50%) of the borrower + // and receives stETH in return await liquidator.lendingPool.liquidationCall( stETH.address, weth.address, @@ -205,73 +315,83 @@ describe('AStETH Liquidation', function () { true ); - const liquidatorWethBalance = await liquidator.wethBalance(); - const liquidatorAstEthBalance = await liquidator.astEthBalance(); - assertBalance(liquidatorWethBalance, wei(0.5), EPSILON); - assertBalance( - liquidatorAstEthBalance, - new BigNumber(wei(2.5)) - .dividedBy(0.8) // price drop - .multipliedBy(1.075) // liquidation bonus + // validate that was withdrawn correct amount of WETH from the liquidator. + // Amount of WETH withdrawn from liquidator might be 1 wei less than borrowAmount / 2 + // because due to shares mechanics borrower might have on balance 1 wei less astETH + asserts.gte( + await liquidator.wethBalance(), + new BigNumber(liquidatorWethBalance).minus(expectedLiquidationAmount).toFixed(0, 1) + ); + + // validate that liquidator received correct amount of astETH + await asserts.astEthBalance( + liquidator, + new BigNumber(expectedLiquidationAmount) + .percentMul(LIQUIDATION_BONUS) + .multipliedBy(wei`1 ether`) + .dividedBy(price.toString()) .toFixed(0, 1), - EPSILON + '2' ); - const userReserveDataAfterLiquidation = await aave.protocolDataProvider.getUserReserveData( + const { currentVariableDebt } = await aave.protocolDataProvider.getUserReserveData( weth.address, borrower.address ); - const userGlobalDataAfterLiquidation = await borrower.lendingPool.getUserAccountData( - borrower.address - ); - expect(userGlobalDataAfterLiquidation.healthFactor.toString()).to.be.bignumber.gt(wei(1)); - assertBalance( - userReserveDataAfterLiquidation.currentVariableDebt.toString(), - wei(2.5), + + // validate that were burned correct amount of debt tokens + asserts.gte( + currentVariableDebt.toString(), + new BigNumber(borrowAmount).minus(expectedLiquidationAmount).toString(), EPSILON ); }); it('Realistic rebase scenario', async () => { - const { stETH, weth, lenders, aave, priceFeed } = setup; - const depositor = lenders.lenderA; + const { stETH, weth, lenders, priceFeed, aave } = setup; const borrower = lenders.lenderB; const liquidator = lenders.lenderC; - // lenderA deposits stETH - await depositor.depositStEth(wei(100)); - // lenderB deposits stETH - await borrower.depositStEth(wei(20)); - // lenderA borrows weth with stETH as collateral - await borrower.lendingPool.borrow( - weth.address, - wei(14), - RateMode.Variable, - '0', - borrower.address - ); + + // borrower deposits stETH to use as collateral + await borrower.depositStEth(wei`20.3 ether`); + await asserts.astEthBalance(borrower, wei`20.3 ether`); + + const borrowAmount = wei`14 ether`; + await borrower.borrowWethVariable(borrowAmount); + + // validate that health factor is above 1 let borrowerGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); - expect(borrowerGlobalData.healthFactor.toString()).to.be.bignumber.gt( - wei(1), - 'Health Factor Below 1' - ); - // before negative rebase price drop happens (3%) still not enough to close positions - await priceFeed.setPrice(wei(0.97)); + asserts.gt(borrowerGlobalData.healthFactor.toString(), wei`1 ether`); + + // before negative rebase price drop happens (current price diff is 4%). + // It's still not enough to close positions + await priceFeed.setPrice(wei`0.96 ether`); borrowerGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); - expect(borrowerGlobalData.healthFactor.toString()).to.be.bignumber.gt( - wei(1), - 'Health Factor Below 1' - ); + asserts.gt(borrowerGlobalData.healthFactor.toString(), wei`1 ether`); + // negative rebase happens (-5%) await setup.rebaseStETH(-0.05); borrowerGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); - expect(borrowerGlobalData.healthFactor.toString()).to.be.bignumber.lt( - wei(1), - 'Health Factor Below 1' + + // validate that after negative rebase health factor became below 1 + asserts.lt(borrowerGlobalData.healthFactor.toString(), wei`1 ether`); + + // validate liquidator had no astETH before liquidation + await asserts.astEthBalance(liquidator, '0'); + + // liquidator deposits 8 weth to make liquidation + const liquidatorWethBalance = wei`8 ether`; + await liquidator.weth.deposit({ value: liquidatorWethBalance }); + + // set allowance for lending pool to withdraw WETH from liquidator + const expectedLiquidationAmount = new BigNumber(borrowAmount).div(2).toFixed(0); + await liquidator.weth.approve( + liquidator.lendingPool.address, + new BigNumber(expectedLiquidationAmount).plus(EPSILON).toFixed() ); - // now position might be closed it's profitable for liquidator - await liquidator.weth.deposit({ value: wei(8) }); - await liquidator.weth.approve(liquidator.lendingPool.address, wei(8)); + // liquidator liquidates max allowed amount of debt (50%) of the borrower + // and receives stETH in return const liquidatorStEthBalanceBeforeLiquidation = await liquidator.stEthBalance(); await liquidator.lendingPool.liquidationCall( stETH.address, @@ -280,20 +400,41 @@ describe('AStETH Liquidation', function () { MAX_UINT_AMOUNT, false ); - const liquidatorWethBalance = await liquidator.wethBalance(); - const liquidatorStEthBalance = await liquidator.stEthBalance(); - assertBalance(liquidatorWethBalance, wei(1), EPSILON); - assertBalance( - new BigNumber(liquidatorStEthBalance) + + // validate that was withdrawn correct amount of WETH from liquidator + asserts.lte( + await liquidator.wethBalance(), + new BigNumber(liquidatorWethBalance).minus(expectedLiquidationAmount).toFixed(), + EPSILON + ); + + // validate that liquidator received correct amount of stETH + const price = await aave.priceOracle.getAssetPrice(setup.stETH.address); + asserts.gte( + new BigNumber(await liquidator.stEthBalance()) .minus(liquidatorStEthBalanceBeforeLiquidation) .toFixed(0, 1), - new BigNumber(wei(7)) - .dividedBy(0.97) // price drop factor - .multipliedBy(1.075) // liquidation bonus + new BigNumber(expectedLiquidationAmount) + .percentMul(LIQUIDATION_BONUS) + .multipliedBy(wei`1 ether`) + .dividedBy(price.toString()) .toFixed(0, 1), EPSILON ); - borrowerGlobalData = await borrower.lendingPool.getUserAccountData(borrower.address); - expect(borrowerGlobalData.healthFactor.toString()).to.be.bignumber.gt(wei(1)); + + const [{ healthFactor }, { currentVariableDebt }] = await Promise.all([ + borrower.lendingPool.getUserAccountData(borrower.address), + aave.protocolDataProvider.getUserReserveData(weth.address, borrower.address), + ]); + + // validate that health factor of borrower recovered + asserts.gt(healthFactor.toString(), wei`1 ether`); + + // validate that were burned correct amount of debt tokens + asserts.gte( + currentVariableDebt.toString(), + new BigNumber(borrowAmount).minus(expectedLiquidationAmount).toString(), + EPSILON + ); }); }); diff --git a/test/astETH/astETH-modifiers.spec.ts b/test/astETH/astETH-modifiers.spec.ts index 3a0e99434..6104b3b18 100644 --- a/test/astETH/astETH-modifiers.spec.ts +++ b/test/astETH/astETH-modifiers.spec.ts @@ -37,7 +37,7 @@ describe('AStETH Modifiers', function () { it('Tries to invoke mintToTreasury not being the LendingPool', async () => { const { astETH } = setup; - await expect(astETH.mintToTreasury(wei(100), '1')).to.be.revertedWith( + await expect(astETH.mintToTreasury(wei`100 ether`, '1')).to.be.revertedWith( CT_CALLER_MUST_BE_LENDING_POOL ); }); diff --git a/test/astETH/astETH-permit.spec.ts b/test/astETH/astETH-permit.spec.ts index 0132eff1d..d4627f8c2 100644 --- a/test/astETH/astETH-permit.spec.ts +++ b/test/astETH/astETH-permit.spec.ts @@ -31,7 +31,7 @@ describe('AStETH Permit', () => { const expiration = 0; const nonce = (await astETH._nonces(owner.address)).toNumber(); - const permitAmount = wei(2); + const permitAmount = wei`2 ether`; const msgParams = buildPermitParams( chainId, astETH.address, @@ -74,7 +74,7 @@ describe('AStETH Permit', () => { const deadline = MAX_UINT_AMOUNT; const nonce = (await astETH._nonces(owner.address)).toNumber(); - const permitAmount = wei(2); + const permitAmount = wei`2 ether`; const msgParams = buildPermitParams( chainId, astETH.address, diff --git a/test/astETH/astETH-rebasing.spec.ts b/test/astETH/astETH-rebasing.spec.ts index b490c56a6..e41754cf1 100644 --- a/test/astETH/astETH-rebasing.spec.ts +++ b/test/astETH/astETH-rebasing.spec.ts @@ -1,79 +1,176 @@ -import { assertBalance, wei } from './helpers'; +import asserts from './asserts'; import { setup } from './__setup.spec'; +import { wei } from './helpers'; +import BigNumber from 'bignumber.js'; describe('AStETH Rebasing', function () { it('Positive rebase: must update balances correctly', async () => { const { lenderA } = setup.lenders; - await lenderA.depositStEth(wei(1000)); - await setup.rebaseStETH(0.1); - assertBalance(await lenderA.astEthBalance(), wei(1100)); - assertBalance(await setup.astEthTotalSupply(), wei(1100)); + + await lenderA.depositStEth(wei`1000 ether`); + await asserts.astEthBalance(lenderA, wei`1000 ether`); + await asserts.astEthTotalSupply(setup, wei`1000 ether`); + + await setup.rebaseStETH(0.01); + await asserts.astEthBalance(lenderA, wei`1010 ether`); + await asserts.astEthTotalSupply(setup, wei`1010 ether`); }); it('Negative rebase: must update balances correctly', async () => { const { lenderA } = setup.lenders; - await lenderA.depositStEth(wei(1000)); - await setup.rebaseStETH(-0.1); - assertBalance(await lenderA.astEthBalance(), wei(900)); - assertBalance(await setup.astEthTotalSupply(), wei(900)); + + await lenderA.depositStEth(wei`1000 ether`); + await asserts.astEthBalance(lenderA, wei`1000 ether`); + await asserts.astEthTotalSupply(setup, wei`1000 ether`); + + await setup.rebaseStETH(-0.0093); + await asserts.astEthBalance(lenderA, wei`990.7 ether`); + await asserts.astEthTotalSupply(setup, wei`990.7 ether`); }); it('Neutral rebase: must stay balances same', async () => { const { lenderA } = setup.lenders; - await lenderA.depositStEth(wei(1000)); + + await lenderA.depositStEth(wei`1000 ether`); + await asserts.astEthBalance(lenderA, wei`1000 ether`); + await asserts.astEthTotalSupply(setup, wei`1000 ether`); + await setup.rebaseStETH(0); - assertBalance(await lenderA.astEthBalance(), wei(1000)); - assertBalance(await setup.astEthTotalSupply(), wei(1000)); + await asserts.astEthBalance(lenderA, wei`1000 ether`); + await asserts.astEthTotalSupply(setup, wei`1000 ether`); }); it('Large deposits rebasing: must update balances correctly', async () => { - const { lenderA } = setup.lenders; - const depositAmount = wei(99_999_999); - await setup.stETH.mint(lenderA.address, depositAmount); - await lenderA.depositStEth(depositAmount); - assertBalance(await lenderA.astEthBalance(), depositAmount); + const { lenderA, lenderB, lenderC } = setup.lenders; + + const lenderADepositAmount = wei`99999999 ether`; + await setup.stETH.mint(lenderA.address, lenderADepositAmount); + + await lenderA.depositStEth(lenderADepositAmount); + await asserts.astEthBalance(lenderA, lenderADepositAmount); + await asserts.astEthTotalSupply(setup, lenderADepositAmount); + await setup.rebaseStETH(0.03); - assertBalance(await lenderA.astEthBalance(), wei(102999998.97)); + const expectedLenderABalanceAfterRebase = wei`102999998.97 ether`; + let expectedAstEthTotalSupply = expectedLenderABalanceAfterRebase; + await asserts.astEthBalance(lenderA, expectedLenderABalanceAfterRebase); + await asserts.astEthTotalSupply(setup, expectedAstEthTotalSupply); + + // after large deposit small deposits also must work correctly + const lenderBDepositAmount = wei`2`; + await lenderB.depositStEth(lenderBDepositAmount); + await asserts.astEthBalance(lenderA, expectedLenderABalanceAfterRebase); + await asserts.astEthBalance(lenderB, lenderBDepositAmount); + expectedAstEthTotalSupply = new BigNumber(expectedAstEthTotalSupply) + .plus(lenderBDepositAmount) + .toFixed(0); + await asserts.astEthTotalSupply(setup, expectedAstEthTotalSupply, '2'); + + // medium size deposits works as expected too + const lenderCDepositAmount = wei`10 ether`; + await lenderC.depositStEth(lenderCDepositAmount); + await asserts.astEthBalance(lenderA, expectedLenderABalanceAfterRebase); + await asserts.astEthBalance(lenderB, lenderBDepositAmount); + await asserts.astEthBalance(lenderC, lenderCDepositAmount); + expectedAstEthTotalSupply = new BigNumber(expectedAstEthTotalSupply) + .plus(lenderCDepositAmount) + .toFixed(0); + await asserts.astEthTotalSupply(setup, expectedAstEthTotalSupply, '3'); }); - it('Rebase before first deposit" must mint correct amount of tokens', async () => { - await setup.rebaseStETH(0.1); + it('Rebase before first deposit: must mint correct amount of tokens', async () => { + await setup.rebaseStETH(0.05); const { lenderA } = setup.lenders; - await lenderA.depositStEth(wei(13)); - assertBalance(await lenderA.astEthBalance(), wei(13)); + + await lenderA.depositStEth(wei`13 ether`); + await asserts.astEthBalance(lenderA, wei`13 ether`); + await asserts.astEthTotalSupply(setup, wei`13 ether`); }); it('lenderA deposits 1 stETH, positive rebase, lenderA transfers 1 astETH to lenderB', async () => { const { lenderA, lenderB } = setup.lenders; - await lenderA.depositStEth(wei(1)); - await setup.rebaseStETH(1); - await lenderA.transferAstEth(lenderB.address, wei(1)); + await lenderA.depositStEth(wei`1 ether`); + await asserts.astEthBalance(lenderA, wei`1 ether`); + await asserts.astEthTotalSupply(setup, wei`1 ether`); + + await setup.rebaseStETH(0.015); + await asserts.astEthBalance(lenderA, wei`1.015 ether`); + await asserts.astEthTotalSupply(setup, wei`1.015 ether`); - assertBalance(await lenderA.astEthBalance(), wei(1)); - assertBalance(await lenderB.astEthBalance(), wei(1)); + await lenderA.transferAstEth(lenderB.address, wei`1 ether`); + await asserts.astEthBalance(lenderA, wei`0.015 ether`); + await asserts.astEthBalance(lenderB, wei`1 ether`); + await asserts.astEthTotalSupply(setup, wei`1.015 ether`); }); it('lenderA deposits 1 stETH, negative rebase, lenderA transfers 0.5 astETH to lenderB', async () => { const { lenderA, lenderB } = setup.lenders; - await lenderA.depositStEth(wei(1)); - await setup.rebaseStETH(-0.5); - await lenderA.transferAstEth(lenderB.address, wei(0.5)); + await lenderA.depositStEth(wei`1 ether`); + await asserts.astEthBalance(lenderA, wei`1 ether`); + await asserts.astEthTotalSupply(setup, wei`1 ether`); - assertBalance(await lenderA.astEthBalance(), wei(0)); - assertBalance(await lenderB.astEthBalance(), wei(0.5)); + await setup.rebaseStETH(-0.02); + await asserts.astEthBalance(lenderA, wei`0.98 ether`); + await asserts.astEthTotalSupply(setup, wei`0.98 ether`); + + await lenderA.transferAstEth(lenderB.address, wei`0.5 ether`); + await asserts.astEthBalance(lenderA, wei`0.48 ether`); + await asserts.astEthBalance(lenderB, wei`0.5 ether`); + await asserts.astEthTotalSupply(setup, wei`0.98 ether`); }); it('lenderA deposits, positive rebase, lenderB deposits', async () => { const { lenderA, lenderB } = setup.lenders; - await lenderA.depositStEth(wei(97)); - await setup.rebaseStETH(1); - await lenderB.depositStEth(wei(13)); + await lenderA.depositStEth(wei`97 ether`); + await asserts.astEthBalance(lenderA, wei`97 ether`); + await asserts.astEthTotalSupply(setup, wei`97 ether`); + + await setup.rebaseStETH(0.0003); + await asserts.astEthBalance(lenderA, wei`97.0291 ether`); + await asserts.astEthTotalSupply(setup, wei`97.0291 ether`); + + await lenderB.depositStEth(wei`13 ether`); + await asserts.astEthBalance(lenderB, wei`13 ether`); + await asserts.astEthBalance(lenderA, wei`97.0291 ether`); + await asserts.astEthTotalSupply(setup, wei`110.0291 ether`, '2'); + }); + + it('lenderA deposits 10 stETH, lenderA transfer 4 stETH to lenderB, positive rebase', async () => { + const { lenderA, lenderB } = setup.lenders; + + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderA.transferAstEth(lenderB.address, wei`4 ether`); + await asserts.astEthBalance(lenderA, wei`6 ether`); + await asserts.astEthBalance(lenderB, wei`4 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await setup.rebaseStETH(+0.02); + await asserts.astEthBalance(lenderA, wei`6.12 ether`); + await asserts.astEthBalance(lenderB, wei`4.08 ether`); + await asserts.astEthTotalSupply(setup, wei`10.2 ether`); + }); + + it('lenderA deposits 10 stETH, lenderA transfer 4 to lenderB, negative rebase', async () => { + const { lenderA, lenderB } = setup.lenders; + + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderA.transferAstEth(lenderB.address, wei`4 ether`); + await asserts.astEthBalance(lenderA, wei`6 ether`); + await asserts.astEthBalance(lenderB, wei`4 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); - assertBalance(await lenderA.astEthBalance(), wei(194)); - assertBalance(await lenderB.astEthBalance(), wei(13)); - assertBalance(await setup.astEthTotalSupply(), wei(207)); + await setup.rebaseStETH(-0.05); + await asserts.astEthBalance(lenderA, wei`5.7 ether`); + await asserts.astEthBalance(lenderB, wei`3.8 ether`); + await asserts.astEthTotalSupply(setup, wei`9.5 ether`); }); }); diff --git a/test/astETH/astETH-transfers.spec.ts b/test/astETH/astETH-transfers.spec.ts index 1104673f5..c680e6440 100644 --- a/test/astETH/astETH-transfers.spec.ts +++ b/test/astETH/astETH-transfers.spec.ts @@ -1,59 +1,109 @@ +import BigNumber from 'bignumber.js'; import { expect } from 'chai'; -import { assertBalance, ONE_RAY, wei } from './helpers'; +import asserts from './asserts'; +import { ONE_RAY, wei } from './helpers'; import { setup } from './__setup.spec'; describe('AStETH Transfers', function () { it('Transfer all tokens: must update balances correctly', async () => { const { lenderA, lenderB } = setup.lenders; - const transferAmount = wei(10); - await lenderA.depositStEth(transferAmount); - await lenderA.transferAstEth(lenderB.address, transferAmount); + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); - assertBalance(await lenderA.astEthBalance(), wei(0)); - assertBalance(await lenderB.astEthBalance(), transferAmount); - assertBalance(await setup.astETH.totalSupply().then(wei), transferAmount); + // after deposit user might receive 10 ether - 1 wei of asETH. + // So in transfer below we retrieve actual user balance to transfer all user's tokens + // which might be 10 ether or 10 ether - 1 wei + await lenderA.transferAstEth(lenderB.address, await lenderA.astEthBalance()); + await asserts.astEthBalance(lenderA, '1'); + // Due to stEth shares mechanic it might be possible, that lenderA transfered + // to lenderB not all his balance but 1 wei less. Taking into consideration + // that after lenderA deposited 10 ether, he also might lost 1 wei, max difference + // between expected and actual balance might be 2 wei or less. + await asserts.astEthBalance(lenderB, wei`10 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`10 ether`); }); it('Transfer part of tokens: must update balances correctly', async () => { const { lenderA, lenderB } = setup.lenders; - await lenderA.depositStEth(wei(10)); - await lenderA.transferAstEth(lenderB.address, wei(5)); + await lenderA.depositStEth(wei`10 ether`); // 10 or 9.9..99 + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); - assertBalance(await lenderA.astEthBalance(), wei(5)); - assertBalance(await lenderB.astEthBalance(), wei(5)); - assertBalance(await setup.astETH.totalSupply().then(wei), wei(10)); + await lenderA.transferAstEth(lenderB.address, wei`5 ether`); + // after lenderA deposit and transfer max possible amount of + // tokens he might has is 5 ether + 1 wei (deposit all 10 ether and transfer 5 ether - 1 wei). + // The min possible amount or astETH is 5 ether - 2 wei (deposit 10 ether - 1 wei and transfer 5 ether - 1 wei) + await asserts.astEthBalance(lenderA, new BigNumber(wei`5 ether`).plus(1).toFixed(0), '2'); + await asserts.astEthBalance(lenderB, wei`5 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); }); - it('Transfer From', async () => { + it('Transfer From: transfer all tokens', async () => { const { lenderA, lenderB } = setup.lenders; - await lenderA.depositStEth(wei(10)); - await lenderA.astETH.approve(lenderB.address, wei(10)); - await lenderB.astETH.transferFrom(lenderA.address, lenderB.address, wei(10)); - assertBalance(await lenderB.astEthBalance(), wei(10)); - assertBalance(await lenderA.astEthBalance(), wei(0)); + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderA.astETH.approve(lenderB.address, await lenderA.astEthBalance()); + await lenderB.astETH.transferFrom( + lenderA.address, + lenderB.address, + await lenderA.astEthBalance() + ); + + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthBalance(lenderB, wei`10 ether`, '2'); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + }); + + it('Transfer From: transfer part of tokens', async () => { + const { lenderA, lenderB } = setup.lenders; + + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderA.astETH.approve(lenderB.address, wei`5 ether`); + await lenderB.astETH.transferFrom(lenderA.address, lenderB.address, wei`5 ether`); + + await asserts.astEthBalance(lenderA, wei`5 ether`); + await asserts.astEthBalance(lenderB, wei`5 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); }); it('Transfer more than deposited: must revert', async () => { const { lenderA, lenderB } = setup.lenders; - await lenderA.depositStEth(wei(10)); - await expect(lenderA.transferAstEth(lenderB.address, wei(11))).to.be.revertedWith( - 'transfer amount exceeds balance' - ); + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + + const lenderAAstEthAmount = await lenderA.astEthBalance(); + + await expect( + lenderA.transferAstEth(lenderB.address, new BigNumber(lenderAAstEthAmount).plus(1).toFixed(0)) + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); + + await expect( + lenderA.transferAstEth( + lenderB.address, + new BigNumber(lenderAAstEthAmount).plus('100').toFixed(0) + ) + ).to.be.revertedWith('ERC20: transfer amount exceeds balance'); }); it('Transfer Events', async () => { const { lenderA, lenderB } = setup.lenders; - const transferAmount = wei(10); - await lenderA.depositStEth(transferAmount); + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); - await expect(lenderA.transferAstEth(lenderB.address, transferAmount)) + await expect(lenderA.transferAstEth(lenderB.address, wei`5 ether`)) .to.emit(setup.astETH, 'BalanceTransfer') - .withArgs(lenderA.address, lenderB.address, transferAmount, ONE_RAY) + .withArgs(lenderA.address, lenderB.address, wei`5 ether`, ONE_RAY) .emit(setup.astETH, 'Transfer') - .withArgs(lenderA.address, lenderB.address, transferAmount); + .withArgs(lenderA.address, lenderB.address, wei`5 ether`); }); }); diff --git a/test/astETH/astETH-views.spec.ts b/test/astETH/astETH-views.spec.ts index 8faa4498d..7329d66e3 100644 --- a/test/astETH/astETH-views.spec.ts +++ b/test/astETH/astETH-views.spec.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; -import { assertBalance, wei } from './helpers'; +import asserts from './asserts'; +import { toWei, wei } from './helpers'; import { setup } from './__setup.spec'; describe('AStETH Views', function () { @@ -12,27 +13,32 @@ describe('AStETH Views', function () { it('scaledBalanceOf', async () => { const { lenderA } = setup.lenders; const scaledBalanceBefore = await lenderA.astETH.scaledBalanceOf(lenderA.address); - assertBalance(scaledBalanceBefore.toString(), wei(0)); - await lenderA.depositStEth(wei(10)); + asserts.eq(scaledBalanceBefore.toString(), wei`0`); + + await lenderA.depositStEth(wei`10 ether`); let scaledBalanceAfter = await lenderA.astETH.scaledBalanceOf(lenderA.address); - assertBalance(scaledBalanceAfter.toString(), wei(10)); + asserts.lte(scaledBalanceAfter.toString(), wei`10 ether`); + await setup.rebaseStETH(0.6); // rebase 60% - await lenderA.depositStEth(wei(10)); + await lenderA.depositStEth(wei`10 ether`); + scaledBalanceAfter = await lenderA.astETH.scaledBalanceOf(lenderA.address); - assertBalance(scaledBalanceAfter.toString(), wei(26)); + asserts.lte(scaledBalanceAfter.toString(), wei`26 ether`); }); it('scaledTotalSupply', async () => { const { lenderA } = setup.lenders; const scaledTotalSupplyBefore = await lenderA.astETH.scaledTotalSupply(); - assertBalance(scaledTotalSupplyBefore.toString(), wei(0)); - await lenderA.depositStEth(wei(10)); + asserts.lte(scaledTotalSupplyBefore.toString(), wei`0`); + + await lenderA.depositStEth(wei`10 ether`); let scaledTotalSupplyAfter = await lenderA.astETH.scaledTotalSupply(); - assertBalance(scaledTotalSupplyAfter.toString(), wei(10)); + asserts.lte(scaledTotalSupplyAfter.toString(), wei`10 ether`); + await setup.rebaseStETH(0.6); // rebase 60% - await lenderA.depositStEth(wei(10)); + await lenderA.depositStEth(wei`10 ether`); scaledTotalSupplyAfter = await lenderA.astETH.scaledTotalSupply(); - assertBalance(scaledTotalSupplyAfter.toString(), wei(26)); + asserts.lte(scaledTotalSupplyAfter.toString(), wei`26 ether`); }); it('getScaledUserBalanceAndSupply', async () => { @@ -41,52 +47,79 @@ describe('AStETH Views', function () { 0: scaledBalanceBefore, 1: scaledTotalSupplyBefore, } = await lenderA.astETH.getScaledUserBalanceAndSupply(lenderA.address); - assertBalance(scaledBalanceBefore.toString(), wei(0)); - assertBalance(scaledTotalSupplyBefore.toString(), wei(0)); - await lenderA.depositStEth(wei(10)); - await lenderB.depositStEth(wei(5)); + asserts.lte(scaledBalanceBefore.toString(), wei`0`); + asserts.lte(scaledTotalSupplyBefore.toString(), wei`0`); + await lenderA.depositStEth(wei`10 ether`); + await lenderB.depositStEth(wei`5 ether`); let { 0: scaledBalanceAfter, 1: scaledTotalSupplyAfter, } = await lenderA.astETH.getScaledUserBalanceAndSupply(lenderA.address); + asserts.lte(scaledBalanceAfter.toString(), wei`10 ether`); + asserts.lte(scaledTotalSupplyAfter.toString(), wei`15 ether`, '2'); - assertBalance(scaledBalanceAfter.toString(), wei(10)); - assertBalance(scaledTotalSupplyAfter.toString(), wei(15)); - await setup.rebaseStETH(0.6); // rebase 60% - await lenderA.depositStEth(wei(10)); + await setup.rebaseStETH(0.006); // rebase 0.6% + await lenderA.depositStEth(wei`10 ether`); let { 0: scaledBalanceAfterRebase, 1: scaledTotalSupplyAfterRebase, } = await lenderA.astETH.getScaledUserBalanceAndSupply(lenderA.address); scaledBalanceAfter = await lenderA.astETH.scaledBalanceOf(lenderA.address); - assertBalance(scaledBalanceAfterRebase.toString(), wei(26)); - assertBalance(scaledTotalSupplyAfterRebase.toString(), wei(34)); + asserts.lte(scaledBalanceAfterRebase.toString(), wei`20.06 ether`, '2'); + asserts.lte(scaledTotalSupplyAfterRebase.toString(), wei`25.09 ether`, '3'); }); it('internalBalanceOf', async () => { - const { lenderA } = setup.lenders; + const { lenderA, lenderB, lenderC } = setup.lenders; const internalBalanceBefore = await lenderA.astETH.internalBalanceOf(lenderA.address); - assertBalance(internalBalanceBefore.toString(), wei(0)); - await lenderA.depositStEth(wei(10)); - let internalBalanceAfter = await lenderA.astETH.internalBalanceOf(lenderA.address); - assertBalance(internalBalanceAfter.toString(), wei(10)); - await setup.rebaseStETH(0.6); // rebase 60% - await lenderA.depositStEth(wei(10)); - internalBalanceAfter = await lenderA.astETH.internalBalanceOf(lenderA.address); - assertBalance(internalBalanceAfter.toString(), wei(16.25)); + asserts.eq(internalBalanceBefore.toString(), wei`0`); + + await lenderA.depositStEth(wei`10 ether`); + let lenderAInternalBalance = await lenderA.astETH.internalBalanceOf(lenderA.address); + asserts.eq(lenderAInternalBalance.toString(), await setup.toInternalBalance(wei`10 ether`)); + + await setup.rebaseStETH(0.07); // rebase 7% + await lenderA.depositStEth(wei`10 ether`); + lenderAInternalBalance = await lenderA.astETH.internalBalanceOf(lenderA.address); + asserts.eq(lenderAInternalBalance.toString(), await setup.toInternalBalance(wei`20.7 ether`)); + + // validate that after flash loan internal balance hasn't changed + await lenderB.makeStEthFlashLoanMode0(wei`15 ether`); + asserts.eq( + lenderAInternalBalance.toString(), + await lenderA.astETH.internalBalanceOf(lenderA.address).then(toWei) + ); + + await lenderC.depositStEth(wei`5 ether`); + asserts.eq( + await lenderC.astETH.internalBalanceOf(lenderC.address).then(toWei), + await setup.toInternalBalance(wei`5 ether`) + ); }); it('internalTotalSupply', async () => { - const { lenderA } = setup.lenders; + const { lenderA, lenderB, lenderC } = setup.lenders; const internalTotalSupplyBefore = await lenderA.astETH.internalTotalSupply(); - assertBalance(internalTotalSupplyBefore.toString(), wei(0)); - await lenderA.depositStEth(wei(10)); + asserts.eq(internalTotalSupplyBefore.toString(), wei`0`); + + await lenderA.depositStEth(wei`10 ether`); let internalTotalSupplyAfter = await lenderA.astETH.internalTotalSupply(); - assertBalance(internalTotalSupplyAfter.toString(), wei(10)); - await setup.rebaseStETH(0.6); // rebase 60% - await lenderA.depositStEth(wei(10)); + asserts.eq(internalTotalSupplyAfter.toString(), await setup.toInternalBalance(wei`10 ether`)); + + await setup.rebaseStETH(0.013); // rebase 1.3% + await lenderA.depositStEth(wei`10 ether`); internalTotalSupplyAfter = await lenderA.astETH.internalTotalSupply(); - assertBalance(internalTotalSupplyAfter.toString(), wei(16.25)); + asserts.eq( + internalTotalSupplyAfter.toString(), + await setup.toInternalBalance(wei`20.13 ether`) + ); + + // validate that after flash loan internal totalSupply hasn't changed + await lenderB.makeStEthFlashLoanMode0(wei`15 ether`); + asserts.eq( + internalTotalSupplyAfter.toString(), + await lenderA.astETH.internalTotalSupply().then(toWei) + ); }); }); diff --git a/test/astETH/astETH-withdrawals.spec.ts b/test/astETH/astETH-withdrawals.spec.ts new file mode 100644 index 000000000..6d71c6b57 --- /dev/null +++ b/test/astETH/astETH-withdrawals.spec.ts @@ -0,0 +1,163 @@ +import { expect } from 'chai'; +import { MAX_UINT_AMOUNT } from '../../helpers/constants'; +import { ProtocolErrors } from '../../helpers/types'; +import { wei } from './helpers'; +import { setup } from './__setup.spec'; +import asserts from './asserts'; +import BigNumber from 'bignumber.js'; + +describe('AStETH Withdrawals:', function () { + it('Withdraw all max uint256: should withdraw correct amount', async () => { + const { lenderA } = setup.lenders; + + await lenderA.depositStEth(wei`10 ether`); + const lenderABalanceAfterDeposit = await lenderA.stEthBalance(); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderA.withdrawStEth(MAX_UINT_AMOUNT); + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthTotalSupply(setup, '1'); + await asserts.lte( + await lenderA.stEthBalance(), + new BigNumber(lenderABalanceAfterDeposit).plus(wei`10 ether`).toFixed(0), + '2' + ); + }); + + it('Withdraw all sum: should withdraw correct amount', async () => { + const { lenderA } = setup.lenders; + + await lenderA.depositStEth(wei`10 ether`); + const lenderABalanceAfterDeposit = await lenderA.stEthBalance(); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderA.withdrawStEth(await lenderA.astEthBalance()); + await asserts.astEthBalance(lenderA, '1'); + await asserts.astEthTotalSupply(setup, '1'); + await asserts.lte( + await lenderA.stEthBalance(), + new BigNumber(lenderABalanceAfterDeposit).plus(wei`10 ether`).toFixed(0), + '2' + ); + }); + + it('Partial withdraw: should withdraw correct amount', async () => { + const { lenderA } = setup.lenders; + + await lenderA.depositStEth(wei`10 ether`); + const lenderABalanceAfterDeposit = await lenderA.stEthBalance(); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderA.withdrawStEth(wei`5 ether`); + await asserts.astEthBalance(lenderA, wei`5 ether`); + await asserts.astEthTotalSupply(setup, wei`5 ether`); + await asserts.lte( + await lenderA.stEthBalance(), + new BigNumber(lenderABalanceAfterDeposit).plus(wei`5 ether`).toFixed(0), + '2' + ); + }); + + it('Multiple withdraws: should withdraw correct amount', async () => { + const { lenderA, lenderB } = setup.lenders; + + await lenderA.depositStEth(wei`10 ether`); + const lenderABalanceAfterDeposit = await lenderA.stEthBalance(); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthBalance(lenderB, wei`0`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderB.depositStEth(wei`20 ether`); + const lenderBBalanceAfterDeposit = await lenderB.stEthBalance(); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthBalance(lenderB, wei`20 ether`); + await asserts.astEthTotalSupply(setup, wei`30 ether`, '2'); + + await lenderA.withdrawStEth(wei`5 ether`); + // after withdraw user can still hold one share on balance, so we count it here + const lenderAExpectedAstEthBalanceAfterWithdraw = new BigNumber(wei`5 ether`) + .plus(1) + .toString(); + await asserts.astEthBalance(lenderA, lenderAExpectedAstEthBalanceAfterWithdraw); + await asserts.astEthBalance(lenderB, wei`20 ether`); + await asserts.astEthTotalSupply(setup, wei`25 ether`, '2'); + await asserts.lte( + await lenderA.stEthBalance(), + new BigNumber(lenderABalanceAfterDeposit).plus(wei`5 ether`).toFixed(0), + '2' + ); + + await lenderB.withdrawStEth(MAX_UINT_AMOUNT); + await asserts.astEthBalance(lenderA, lenderAExpectedAstEthBalanceAfterWithdraw); + await asserts.astEthBalance(lenderB, '1'); + // after two withdraws astETH may still has couple of shares + const expectedAstEthTotalSupply = new BigNumber(wei`5 ether`).plus(2).toFixed(0); + await asserts.astEthTotalSupply(setup, expectedAstEthTotalSupply, '2'); + await asserts.lte( + await lenderB.stEthBalance(), + new BigNumber(lenderBBalanceAfterDeposit).plus(wei`20 ether`).toFixed(0), + '2' + ); + }); + + it('Withdraw after rebase: should withdraw correct amount', async () => { + const { lenderA, lenderB } = setup.lenders; + + await lenderA.depositStEth(wei`10 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthBalance(lenderB, wei`0`); + await asserts.astEthTotalSupply(setup, wei`10 ether`); + + await lenderB.depositStEth(wei`20 ether`); + await asserts.astEthBalance(lenderA, wei`10 ether`); + await asserts.astEthBalance(lenderB, wei`20 ether`); + await asserts.astEthTotalSupply(setup, wei`30 ether`, '2'); + + // positive rebase + await setup.rebaseStETH(0.1); + const lenderABalanceAfterRebase = await lenderA.stEthBalance(); + const lenderBBalanceAfterRebase = await lenderB.stEthBalance(); + await asserts.astEthBalance(lenderA, wei`11 ether`); + await asserts.astEthBalance(lenderB, wei`22 ether`); + await asserts.astEthTotalSupply(setup, wei`33 ether`, '2'); + + await lenderA.withdrawStEth(wei`10 ether`); + // after withdraw user can still hold one share on balance, so we count it here + const lenderAExpectedAstEthBalanceAfterWithdraw = new BigNumber(wei`1 ether`) + .plus(1) + .toString(); + await asserts.astEthBalance(lenderA, lenderAExpectedAstEthBalanceAfterWithdraw); + await asserts.astEthBalance(lenderB, wei`22 ether`); + await asserts.astEthTotalSupply(setup, wei`23 ether`, '2'); + await asserts.lte( + await lenderA.stEthBalance(), + new BigNumber(lenderABalanceAfterRebase).plus(wei`10 ether`).toFixed(0), + '2' + ); + + await lenderB.withdrawStEth(MAX_UINT_AMOUNT); + await asserts.astEthBalance(lenderA, lenderAExpectedAstEthBalanceAfterWithdraw); + await asserts.astEthBalance(lenderB, '1'); + // after two withdraws astETH may still has couple of shares + const expectedAstEthTotalSupply = new BigNumber(wei`1 ether`).plus(2).toFixed(0); + await asserts.astEthTotalSupply(setup, expectedAstEthTotalSupply, '2'); + await asserts.lte( + await lenderB.stEthBalance(), + new BigNumber(lenderBBalanceAfterRebase).plus(wei`22 ether`).toFixed(0), + '2' + ); + }); + + it('Withdraw scaled amount is zero: should revert with correct message', async () => { + const { lenderA } = setup.lenders; + await lenderA.depositStEth(wei`1 ether`); + // rebase 200% + await setup.rebaseStETH(2); + // try to withdraw 1 wei after rebase happened + // which will be 0 after scaling + await expect(lenderA.withdrawStEth(1)).to.revertedWith(ProtocolErrors.CT_INVALID_BURN_AMOUNT); + }); +}); diff --git a/test/astETH/astETH-withdraws.spec.ts b/test/astETH/astETH-withdraws.spec.ts deleted file mode 100644 index 5a41ffa89..000000000 --- a/test/astETH/astETH-withdraws.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import hre from 'hardhat'; -import { expect } from 'chai'; -import { zeroAddress } from 'ethereumjs-util'; -import { MAX_UINT_AMOUNT } from '../../helpers/constants'; -import { ProtocolErrors } from '../../helpers/types'; -import { assertBalance, ONE_RAY, wei } from './helpers'; -import { setup } from './__setup.spec'; - -describe('AStETH Withdraws:', function () { - it('Withdraw all max uint256: should withdraw correct amount', async () => { - const { lenderA } = setup.lenders; - await lenderA.depositStEth(wei(10)); - await withdrawStEthAndValidate(lenderA); - }); - it('Withdraw all sum: should withdraw correct amount', async () => { - const { lenderA } = setup.lenders; - await lenderA.depositStEth(wei(10)); - await withdrawStEthAndValidate(lenderA, wei(10)); - }); - it('Partial withdraw: should withdraw correct amount', async () => { - const { lenderA } = setup.lenders; - await lenderA.depositStEth(wei(10)); - await withdrawStEthAndValidate(lenderA, wei(5)); - }); - it('Multiple withdraws: should withdraw correct amount', async () => { - const { lenderA, lenderB } = setup.lenders; - await Promise.all([lenderA.depositStEth(wei(10)), lenderB.depositStEth(wei(20))]); - await withdrawStEthAndValidate(lenderA, wei(5)); - await withdrawStEthAndValidate(lenderB); - }); - it('Withdraw after rebase: should withdraw correct amount', async () => { - const { lenderA, lenderB } = setup.lenders; - await Promise.all([lenderA.depositStEth(wei(10)), lenderB.depositStEth(wei(20))]); - // positive rebase - await setup.rebaseStETH(0.1); - assertBalance(await lenderA.astEthBalance(), wei(11)); - assertBalance(await lenderB.astEthBalance(), wei(22)); - - await withdrawStEthAndValidate(lenderA, wei(10)); - await withdrawStEthAndValidate(lenderB); - }); - it('Withdraw scaled amount is zero: should revert with correct message', async () => { - const { lenderA } = setup.lenders; - await lenderA.depositStEth(wei(1)); - // rebase 200% - await setup.rebaseStETH(2); - // try to withdraw 1 wei after rebase happened - // which will be 0 after scaling - await expect(lenderA.withdrawStEth(1)).to.revertedWith(ProtocolErrors.CT_INVALID_BURN_AMOUNT); - }); -}); - -async function assertStEthWithdrawEvents(astETH, withdrawer, receiverOfUnderlying, tx, amount) { - await expect(Promise.resolve(tx)) - .to.emit(astETH, 'Transfer') - .withArgs(withdrawer.address, zeroAddress(), amount) - .to.emit(astETH, 'Burn') - .withArgs(withdrawer.address, receiverOfUnderlying.address, amount, ONE_RAY); -} - -export async function withdrawStEthAndValidate(lender, withdrawAmount = MAX_UINT_AMOUNT) { - const [balanceBefore, totalSupplyBefore] = await Promise.all([ - lender.astEthBalance(), - lender.astETH.totalSupply(), - ]); - const tx = await lender.withdrawStEth(withdrawAmount); - await assertStEthWithdrawEvents( - lender.astETH, - lender, - lender, - tx, - withdrawAmount === MAX_UINT_AMOUNT ? balanceBefore : withdrawAmount - ); - - const expectedBalance = - withdrawAmount === MAX_UINT_AMOUNT - ? '0' - : hre.ethers.BigNumber.from(balanceBefore).sub(withdrawAmount).toString(); - assertBalance(await lender.astEthBalance(), expectedBalance); - - const expectedTotalSupply = hre.ethers.BigNumber.from(totalSupplyBefore) - .sub(withdrawAmount === MAX_UINT_AMOUNT ? balanceBefore : withdrawAmount) - .toString(); - await assertBalance(await lender.astETH.totalSupply().then(wei), expectedTotalSupply); - return tx; -} diff --git a/test/astETH/astEth-deposits.spec.ts b/test/astETH/astEth-deposits.spec.ts deleted file mode 100644 index 8e6bf46f4..000000000 --- a/test/astETH/astEth-deposits.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { expect } from 'chai'; -import { zeroAddress } from 'ethereumjs-util'; -import { ProtocolErrors } from '../../helpers/types'; -import { assertBalance, ONE_RAY, wei } from './helpers'; -import { setup } from './__setup.spec'; - -describe('AStETH deposits', async () => { - it('First deposit: should mint exact amount of AStETH', async () => { - const { lenderA } = setup.lenders; - const depositAmount = wei(10); - await lenderA.depositStEth(depositAmount); - - assertBalance(await lenderA.astEthBalance(), depositAmount); - assertBalance(await setup.astETH.totalSupply().then(wei), depositAmount); - }); - - it('Multiple deposits: should mint exact amount of AStETH ', async () => { - const { lenderA, lenderB } = setup.lenders; - - // lenderA deposits - const lenderADepositAmount = wei(10); - await lenderA.depositStEth(lenderADepositAmount); - - assertBalance(await lenderA.astEthBalance(), lenderADepositAmount); - assertBalance(await setup.astETH.totalSupply().then(wei), lenderADepositAmount); - - // lenderB deposits - const lenderBDepositAmount = wei(5); - await lenderB.depositStEth(lenderBDepositAmount); - - assertBalance(await lenderB.astEthBalance(), lenderBDepositAmount); - assertBalance(await setup.astETH.totalSupply().then(wei), wei(15)); - - // lenderA deposits again - const lenderBSecondDepositAmount = wei(7); - await lenderB.depositStEth(lenderBSecondDepositAmount); - - assertBalance(await lenderB.astEthBalance(), wei(12)); - assertBalance(await setup.astETH.totalSupply().then(wei), wei(22)); - }); - - it('Zero scaled amount is zero: should revert with correct message', async () => { - const { lenderA } = setup.lenders; - // rebase 200% - await setup.rebaseStETH(2); - await expect(lenderA.depositStEth(1)).to.revertedWith(ProtocolErrors.CT_INVALID_MINT_AMOUNT); - }); - - it('Small deposit 100 wei: should mint exact amount of AStETH', async () => { - const { lenderA } = setup.lenders; - const depositAmount = '100'; - await lenderA.depositStEth(depositAmount); - - assertBalance(await lenderA.astEthBalance(), depositAmount); - assertBalance(await setup.astETH.totalSupply().then(wei), depositAmount); - }); - - it('Deposit Events', async () => { - const { lenderA } = setup.lenders; - const depositAmount = wei(10); - await lenderA.depositStEth(depositAmount); - - await expect(lenderA.depositStEth(depositAmount)) - .to.emit(setup.astETH, 'Transfer') - .withArgs(zeroAddress(), lenderA.address, depositAmount) - .emit(setup.astETH, 'Mint') - .withArgs(lenderA.address, depositAmount, ONE_RAY); - }); -}); diff --git a/test/astETH/helpers.ts b/test/astETH/helpers.ts index d192a4c83..12c907411 100644 --- a/test/astETH/helpers.ts +++ b/test/astETH/helpers.ts @@ -11,25 +11,44 @@ interface ReserveDataInfo { const ONE_WAD = '1000000000000000000'; export const ONE_RAY = '1000000000000000000000000000'; -export function wei(amount: number | string | ethers.BigNumber) { - if (hre.ethers.BigNumber.isBigNumber(amount)) { - return amount.toString(); +export function wei(value: TemplateStringsArray) { + if (!value) { + return '0'; + } + const [amountText, unit = 'wei'] = value[0] + .trim() + .split(' ') + .filter((v) => !!v); + if (!Number.isFinite(+amountText)) { + throw new Error(`Amount ${amountText} is not a number`); + } + const amount = new BigNumber(amountText); + + switch (unit) { + case 'wei': + return amount.toFixed(0); + case 'kwei': + return amount.multipliedBy(10 ** 3).toFixed(0); + case 'mwei': + return amount.multipliedBy(10 ** 6).toFixed(0); + case 'gwei': + return amount.multipliedBy(10 ** 9).toFixed(0); + case 'microether': + return amount.multipliedBy(10 ** 12).toFixed(0); + case 'milliether': + return amount.multipliedBy(10 ** 15).toFixed(0); + case 'ether': + return amount.multipliedBy(10 ** 18).toFixed(0); + default: + throw new Error(`Unknown unit "${unit}"`); } - return new BigNumber(ONE_WAD).multipliedBy(amount).toFixed(0, 1); } -export function ray(amount: number | string | ethers.BigNumber) { +export function toWei(amount: number | string | ethers.BigNumber) { if (hre.ethers.BigNumber.isBigNumber(amount)) { return amount.toString(); } - return new BigNumber(amount).multipliedBy(ONE_RAY).toFixed(0, 1); -} - -export function assertBalance(actual: string, expected: string, epsilon: string = '1') { - const lowerBound = new BigNumber(expected).minus(epsilon).toString(); - const upperBound = new BigNumber(expected).plus(epsilon).toString(); - expect(actual).to.be.bignumber.lte(upperBound); - expect(actual).to.be.bignumber.gte(lowerBound); + return new BigNumber(ONE_WAD).multipliedBy(amount).toFixed(0, 1); } export const advanceTimeAndBlock = async function (forwardTime: number) { @@ -52,7 +71,7 @@ export const advanceTimeAndBlock = async function (forwardTime: number) { }; export const expectedBalanceAfterRebase = (balance: string, rebaseAmount: number) => { - return new BigNumber(balance).multipliedBy(1 + rebaseAmount).toFixed(0, 1); + return new BigNumber(balance).multipliedBy(1 + rebaseAmount).toFixed(0, 0); }; export const expectedFlashLoanPremium = (amount: string) => { @@ -62,38 +81,33 @@ export const expectedFlashLoanPremium = (amount: string) => { export const expectedBalanceAfterFlashLoan = ( balanceBeforeFlashLoan: string, - reserveDataBeforeFlashLoan: ReserveDataInfo, + totalSupply: string, flashLoanAmount: string ) => { return new BigNumber(balanceBeforeFlashLoan) .plus( new BigNumber(balanceBeforeFlashLoan).rayMul( - expectedLiquidityIndexGrowAfterFlashLoan(reserveDataBeforeFlashLoan, flashLoanAmount) + expectedLiquidityIndexIncrementAfterFlashLoan(totalSupply, flashLoanAmount) ) ) .toString(); }; export const expectedLiquidityIndexAfterFlashLoan = ( - reserveDataBeforeFlashLoan: ReserveDataInfo, + liquidityIndex: string, + totalSupply: string, flashLoanAmount: string ) => { - const premium = expectedFlashLoanPremium(flashLoanAmount); - const amountToLiquidityRatio = new BigNumber(premium) - .wadToRay() - .rayDiv(new BigNumber(reserveDataBeforeFlashLoan.availableLiquidity.toString()).wadToRay()); - const result = amountToLiquidityRatio + const result = expectedLiquidityIndexIncrementAfterFlashLoan(totalSupply, flashLoanAmount) .plus(new BigNumber(ONE_RAY)) - .rayMul(new BigNumber(reserveDataBeforeFlashLoan.liquidityIndex.toString())); + .rayMul(new BigNumber(liquidityIndex)); return result.toFixed(0, 1); }; -const expectedLiquidityIndexGrowAfterFlashLoan = ( - reserveDataBeforeFlashLoan: ReserveDataInfo, +const expectedLiquidityIndexIncrementAfterFlashLoan = ( + totalSupply: string, flashLoanAmount: string ) => { const premium = expectedFlashLoanPremium(flashLoanAmount); - return new BigNumber(premium) - .wadToRay() - .rayDiv(new BigNumber(reserveDataBeforeFlashLoan.availableLiquidity.toString()).wadToRay()); + return new BigNumber(premium).wadToRay().rayDiv(new BigNumber(totalSupply).wadToRay()); }; diff --git a/test/astETH/init.ts b/test/astETH/init.ts index e990e5bd2..7215f50d8 100644 --- a/test/astETH/init.ts +++ b/test/astETH/init.ts @@ -26,11 +26,12 @@ import { import { AaveContracts, Addresses } from '../../helpers/lido/aave-mainnet-contracts'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'; import { strategySTETH } from '../../markets/aave/reservesConfigs'; -import { expectedFlashLoanPremium, wei } from './helpers'; +import { expectedFlashLoanPremium, toWei, wei } from './helpers'; import BigNumber from 'bignumber.js'; +import { RateMode } from '../../helpers/types'; export class AstEthSetup { - public static readonly INITIAL_BALANCE = wei('1000'); + public static readonly INITIAL_BALANCE = wei`1000 ether`; private constructor( public readonly deployer: SignerWithAddress, public readonly aave: AaveContracts, @@ -41,7 +42,7 @@ export class AstEthSetup { public readonly variableDebtStETH: VariableDebtStETH, public readonly priceFeed: ChainlinkAggregatorMock, public readonly lenders: { lenderA: Lender; lenderB: Lender; lenderC: Lender }, - public readonly flashLoanReceiverLoan: FlashLoanReceiverMock + public readonly flashLoanReceiverMock: FlashLoanReceiverMock ) {} static async deploy(): Promise { @@ -135,16 +136,32 @@ export class AstEthSetup { async rebaseStETH(perc) { const currentTotalSupply = await this.stETH.totalSupply(); const currentSupply = new BigNumber(currentTotalSupply.toString()); - const supplyDelta = currentSupply.multipliedBy(Number(perc * 10000).toFixed(0)).div(10000); + const percentBasis = 1_000_000_000_000_000; + const supplyDelta = currentSupply + .multipliedBy(Number(perc * percentBasis).toFixed(0)) + .div(percentBasis); if (supplyDelta.isNegative()) { - await this.stETH.negativeRebase(supplyDelta.negated().toFixed()); + await this.stETH.negativeRebase(supplyDelta.negated().toFixed(0)); } else { - await this.stETH.positiveRebase(supplyDelta.toFixed()); + await this.stETH.positiveRebase(supplyDelta.toFixed(0)); } } astEthTotalSupply() { - return this.astETH.totalSupply().then(wei); + return this.astETH.totalSupply().then(toWei); + } + + astEthInternalTotalSupply() { + return this.astETH.internalTotalSupply().then(toWei); + } + + async toInternalBalance(amount: string) { + const liquidityIndex = await this.aave.lendingPool.getReserveNormalizedIncome( + this.stETH.address + ); + return new BigNumber(await this.stETH.getSharesByPooledEth(amount).then(toWei)) + .rayDiv(new BigNumber(liquidityIndex.toString())) + .toFixed(0, 1); } } @@ -161,14 +178,14 @@ export class Lender { lendingPool: LendingPool, astETH: AStETH, signer: SignerWithAddress, - mockFlashLoanReceiver: FlashLoanReceiverMock + flashLoanReceiverMock: FlashLoanReceiverMock ) { this.signer = signer; this.weth = weth.connect(signer); this.stETH = stETH.connect(signer); this.lendingPool = lendingPool.connect(signer); this.astETH = astETH.connect(signer); - this.flashLoanReceiverMock = mockFlashLoanReceiver; + this.flashLoanReceiverMock = flashLoanReceiverMock; } get address(): string { @@ -179,24 +196,33 @@ export class Lender { await this.stETH.approve(this.lendingPool.address, amount); return this.lendingPool.deposit(this.stETH.address, amount, this.signer.address, 0); } + withdrawStEth(amount: ethers.BigNumberish) { return this.lendingPool.withdraw(this.stETH.address, amount, this.signer.address); } + async astEthInternalBalance() { + return this.astETH.internalBalanceOf(this.address).then(toWei); + } + wethBalance() { - return this.weth.balanceOf(this.address).then(wei); + return this.weth.balanceOf(this.address).then(toWei); } + stEthBalance() { - return this.stETH.balanceOf(this.address).then(wei); + return this.stETH.balanceOf(this.address).then(toWei); } + astEthBalance() { - return this.astETH.balanceOf(this.address).then(wei); + return this.astETH.balanceOf(this.address).then(toWei); } + async depositWeth(amount: ethers.BigNumberish) { await this.weth.deposit({ value: amount }); await this.weth.approve(this.lendingPool.address, amount); return this.lendingPool.deposit(this.weth.address, amount, this.signer.address, 0); } + transferAstEth(recipient: string, amount: ethers.BigNumberish) { return this.astETH.transfer(recipient, amount); } @@ -226,6 +252,14 @@ export class Lender { return this.makeStEthFlashLoan(2, flashLoanAmount); } + async borrowWethStable(amount: string) { + return this.lendingPool.borrow(this.weth.address, amount, RateMode.Stable, '0', this.address); + } + + async borrowWethVariable(amount: string) { + return this.lendingPool.borrow(this.weth.address, amount, RateMode.Variable, '0', this.address); + } + private async makeStEthFlashLoan(mode: 1 | 2, flashLoanAmount: string) { // deposit collateral await this.depositWeth(new BigNumber(flashLoanAmount).multipliedBy(2).toString());