New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix denial of service borrowing vault #376
Changes from all commits
0b786e8
f1ae94a
99e3f46
ef730bf
c2aed27
20c5bb0
ecfacbe
0ad0438
fa087ea
0fba46a
a92048c
33c62dc
a087364
75c4deb
67c50be
49a5f2e
bb24fc6
0fe9d2a
bfc6e66
50cf119
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.15; | ||
|
||
import "forge-std/console.sol"; | ||
import {Routines} from "../../utils/Routines.sol"; | ||
import {ForkingSetup} from "../ForkingSetup.sol"; | ||
import {AaveV2} from "../../../src/providers/mainnet/AaveV2.sol"; | ||
import {ILendingProvider} from "../../../src/interfaces/ILendingProvider.sol"; | ||
import {BorrowingVault} from "../../../src/vaults/borrowing/BorrowingVault.sol"; | ||
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; | ||
import {IV2Pool} from "../../../src/interfaces/aaveV2/IV2Pool.sol"; | ||
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import {PausableVault} from "../../../src/abstracts/PausableVault.sol"; | ||
|
||
contract DenialOfServiceTest is Routines, ForkingSetup { | ||
using SafeERC20 for IERC20; | ||
|
||
uint256 public constant TREASURY_PK = 0xF; | ||
address public TREASURY = vm.addr(TREASURY_PK); | ||
|
||
ILendingProvider public aaveV2; | ||
|
||
uint256 public TROUBLEMAKER_PK = 0x1122; | ||
address public TROUBLEMAKER = vm.addr(TROUBLEMAKER_PK); | ||
|
||
uint256 public constant DEPOSIT_AMOUNT = 1 ether; | ||
uint256 public constant BORROW_AMOUNT = 800 * 1e6; | ||
|
||
IV2Pool public aaveV2pool; | ||
|
||
function setUp() public { | ||
setUpFork(MAINNET_DOMAIN); | ||
|
||
vm.label(TROUBLEMAKER, "TROUBLEMAKER"); | ||
|
||
aaveV2 = new AaveV2(); | ||
ILendingProvider[] memory providers = new ILendingProvider[](1); | ||
providers[0] = aaveV2; | ||
|
||
deploy(providers); | ||
|
||
aaveV2pool = IV2Pool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9); | ||
} | ||
|
||
function testFail_tryWithdrawWithoutCorrectDebt() public { | ||
// Alice deposits and borrows | ||
do_deposit(DEPOSIT_AMOUNT, vault, ALICE); | ||
do_borrow(BORROW_AMOUNT, vault, ALICE); | ||
|
||
// Troublemaker pays vaults debt | ||
deal(debtAsset, TROUBLEMAKER, BORROW_AMOUNT * 10); | ||
vm.startPrank(TROUBLEMAKER); | ||
IERC20(debtAsset).safeApprove(address(aaveV2pool), BORROW_AMOUNT); | ||
aaveV2pool.repay(vault.debtAsset(), BORROW_AMOUNT, 2, address(vault)); | ||
vm.stopPrank(); | ||
|
||
assertEq(vault.balanceOf(ALICE), DEPOSIT_AMOUNT); | ||
assertEq(vault.balanceOfDebt(ALICE), 0); | ||
|
||
//Withdraw will fail until debt is corrected and withdraw is unpaused | ||
uint256 maxAmount = vault.maxWithdraw(ALICE); | ||
do_withdraw(maxAmount, vault, ALICE); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going through the test I realized that in fact if some "troublemaker" paybacks the debt of the vault, users are now free to withdraw their assets without burning their debtShares. I think we need to discuss what behavior we expect. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the time in between us calling the troublemaker paying the debt and we calling correctDebt we'll be a problem. I'll come up with some possible solutions so we can discuss them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, I paused the withdraw in the withdraw function and it is not very efficient but couldnt find a better way. Issue was added to the optimizations card in the board. |
||
} | ||
|
||
function test_correctDebtUnpauseAndWithdraw() public { | ||
uint256 amountCorrected = BORROW_AMOUNT; | ||
|
||
do_deposit(DEPOSIT_AMOUNT, vault, ALICE); | ||
do_borrow(BORROW_AMOUNT, vault, ALICE); | ||
|
||
// Troublemaker pays vaults debt | ||
deal(debtAsset, TROUBLEMAKER, BORROW_AMOUNT * 10); | ||
vm.startPrank(TROUBLEMAKER); | ||
//TODO check this after rounding issue is fixed | ||
//approve more than needed because of rounding issues | ||
IERC20(debtAsset).safeApprove(address(aaveV2pool), BORROW_AMOUNT + 10); | ||
//pay full amount, sometimes bigger than BORROW_AMOUNT | ||
aaveV2pool.repay( | ||
vault.debtAsset(), aaveV2.getBorrowBalance(address(vault), vault), 2, address(vault) | ||
); | ||
vm.stopPrank(); | ||
|
||
bytes memory data = abi.encodeWithSelector(BorrowingVault.correctDebt.selector, TREASURY); | ||
_callWithTimelock(address(vault), data); | ||
skip(2 days); | ||
|
||
//try to payback and withdraw and check it works | ||
do_payback(BORROW_AMOUNT, vault, ALICE); | ||
do_withdraw(DEPOSIT_AMOUNT, vault, ALICE); | ||
|
||
//DEPOSIT_AMOUNT has built up some interest after the call with timelock (lets assume 1%) | ||
assertApproxEqAbs(vault.balanceOf(ALICE), 0, DEPOSIT_AMOUNT / 100); | ||
|
||
//check the actual corrected amount is in TREASURY | ||
assertEq(aaveV2.getBorrowBalance(address(vault), vault), 0); | ||
assertEq(IERC20(debtAsset).balanceOf(TREASURY), amountCorrected); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you please explain what you intend to do here?
I see a pause on top and a revert added just below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the situation in where some external actor, pays entirely the debt of the vault. By that previously, they will effectively, disable the vault from borrowing operations, if I recall.
Now, if... someone, does pay the vault's debt ever, we created a function to restore debt back, and essentially later decide what to do with the "benefactor" proceeds.
The conditions there, is to avoid withdraws at the moment right after the debt has been paid back.
If you read the conditions: if total debt is zero, meaning... someone paidback the whole debt, and there is debtSharesSupply, meaning.... there were user/s owing funds. Then pause all withdrawals until all gets sort out.
In the case of someone paying off all the debt, we don't want to create "bad debt" position, by allowing users to withdraw the shares and leaving "unbacked" debtShares, once debt is restored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though the revert will do effectively the job. I see now, that state for pause will not persist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep general intention makes sense
I was more concerned with the above, yep it won't persist
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refer to #536