-
Notifications
You must be signed in to change notification settings - Fork 27
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 dust surplus claim in recovery mode liquidation #787
Changes from all commits
c49e5f1
2e0e0e8
12a473e
ecdb55d
6990b14
b322029
2b46535
ef4f90d
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 | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -226,6 +226,7 @@ abstract contract TargetFunctions is Properties { | |||||||||||||||||||||||
_before(_cdpId); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
uint256 _icrToLiq = cdpManager.getSyncedICR(_cdpId, priceFeedMock.getPrice()); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
(success, returnData) = actor.proxy( | ||||||||||||||||||||||||
address(cdpManager), | ||||||||||||||||||||||||
abi.encodeWithSelector(CdpManager.liquidate.selector, _cdpId) | ||||||||||||||||||||||||
|
@@ -234,6 +235,12 @@ abstract contract TargetFunctions is Properties { | |||||||||||||||||||||||
_after(_cdpId); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if (success) { | ||||||||||||||||||||||||
// SURPLUS-CHECK-1 | The surplus is capped at 4 wei | NOTE: Proxy of growth, storage var would further refine | ||||||||||||||||||||||||
if (_icrToLiq <= cdpManager.MCR()) { | ||||||||||||||||||||||||
gte(vars.collSurplusPoolBefore + 4, vars.collSurplusPoolAfter, "SURPLUS-CHECK-1"); | ||||||||||||||||||||||||
gte(vars.userSurplusBefore + 4, vars.userSurplusAfter, "SURPLUS-CHECK-2"); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+238
to
+242
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. The implementation of surplus checks (
- gte(vars.collSurplusPoolBefore + 4, vars.collSurplusPoolAfter, "SURPLUS-CHECK-1");
- gte(vars.userSurplusBefore + 4, vars.userSurplusAfter, "SURPLUS-CHECK-2");
+ const uint SURPLUS_CAP = 4; // Descriptive comment explaining the choice of value
+ gte(vars.collSurplusPoolBefore + SURPLUS_CAP, vars.collSurplusPoolAfter, "SURPLUS-CHECK-1");
+ gte(vars.userSurplusBefore + SURPLUS_CAP, vars.userSurplusAfter, "SURPLUS-CHECK-2"); Committable suggestion
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
// if ICR >= TCR then we ignore | ||||||||||||||||||||||||
// We could check that Liquidated is not above TCR | ||||||||||||||||||||||||
if ( | ||||||||||||||||||||||||
|
@@ -421,6 +428,9 @@ abstract contract TargetFunctions is Properties { | |||||||||||||||||||||||
_after(bytes32(0)); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if (success) { | ||||||||||||||||||||||||
// SURPLUS-CHECK-1 | The surplus is capped at 4 wei | NOTE: We use Liquidate for the exact CDP check | ||||||||||||||||||||||||
gte(vars.collSurplusPoolBefore + 4, vars.collSurplusPoolAfter, "SURPLUS-CHECK-1"); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Cdp[] memory cdpsAfter = _getCdpIdsAndICRs(); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Cdp[] memory cdpsLiquidated = _cdpIdsAndICRsDiff(cdpsBefore, cdpsAfter); | ||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -969,4 +969,47 @@ contract CdpManagerLiquidationTest is eBTCBaseInvariants { | |
|
||
return (cdpIds, _newPrice); | ||
} | ||
|
||
function testSurplusInRMWhenICRBelowMCR() public { | ||
address wallet = users[0]; | ||
|
||
// set eth per stETH share | ||
collateral.setEthPerShare(1158379174506084879); | ||
|
||
// fetch price before open | ||
uint256 oldprice = priceFeedMock.fetchPrice(); | ||
|
||
// open five cdps | ||
_openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 240e16)); | ||
_openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 240e16)); | ||
_openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 240e16)); | ||
_openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 240e16)); | ||
bytes32 underwater = _openTestCDP(wallet, 2e18 + 2e17, ((2e18 * oldprice) / 210e16)); | ||
|
||
// reduce the price by half to make underwater cdp | ||
priceFeedMock.setPrice(oldprice / 2); | ||
|
||
// fetch new price after reduce | ||
uint256 newPrice = priceFeedMock.fetchPrice(); | ||
|
||
// ensure the system is in recovery mode | ||
assert(cdpManager.getSyncedTCR(newPrice) < CCR); | ||
|
||
// liquidate underwater cdp with ICR < MCR | ||
vm.startPrank(wallet); | ||
cdpManager.liquidate(underwater); | ||
vm.stopPrank(); | ||
|
||
// make sure the cdp is no longer in the sorted list | ||
assert(!sortedCdps.contains(underwater)); | ||
|
||
// fetch the surplus after the liquidation | ||
uint256 surplus = collSurplusPool.getSurplusCollShares(wallet); | ||
|
||
// console log the surplus coll | ||
console.log("Surplus:", surplus); | ||
|
||
// ensure that the surplus is zero | ||
assert(surplus == 0); | ||
} | ||
Comment on lines
+972
to
+1014
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. The newly added test function
Overall, the addition of this test function is a positive step towards ensuring the system behaves as expected under specific liquidation scenarios in recovery mode. Addressing the above points can further enhance the quality and coverage of the test suite. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,13 +25,16 @@ contract EToFoundry is | |
{ | ||
modifier setup() override { | ||
_; | ||
address sender = uint160(msg.sender) % 3 == 0 ? address(USER1) : uint160(msg.sender) % 3 == 1 | ||
? address(USER2) | ||
: address(USER3); | ||
actor = actors[sender]; | ||
} | ||
|
||
function setUp() public { | ||
_setUp(); | ||
_setUpActors(); | ||
actor = actors[USER1]; | ||
vm.startPrank(address(actor)); | ||
actor = actors[address(USER1)]; | ||
} | ||
|
||
function _checkTotals() internal { | ||
Comment on lines
25
to
40
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.
AWS Access Key ID Value detected in a comment. This is a sensitive credential and should not be hardcoded here. Even though it's in a comment, it's best practice to avoid including potentially sensitive information in your codebase. Consider removing or obfuscating this information.
AWS Access Key ID Value detected in a comment. This is a sensitive credential and should not be hardcoded here. Even though it's in a comment, it's best practice to avoid including potentially sensitive information in your codebase. Consider removing or obfuscating this information.
AWS Access Key ID Value detected in a comment. This is a sensitive credential and should not be hardcoded here. Even though it's in a comment, it's best practice to avoid including potentially sensitive information in your codebase. Consider removing or obfuscating this information.
AWS Access Key ID Value detected in a comment. This is a sensitive credential and should not be hardcoded here. Even though it's in a comment, it's best practice to avoid including potentially sensitive information in your codebase. Consider removing or obfuscating this information. |
||
|
@@ -221,9 +224,17 @@ contract EToFoundry is | |
function _logStakes() internal { | ||
bytes32 currentCdp = sortedCdps.getFirst(); | ||
|
||
console2.log("=== LogStakes ==="); | ||
|
||
uint256 currentPrice = priceFeedMock.fetchPrice(); | ||
uint256 currentPricePerShare = collateral.getPooledEthByShares(1 ether); | ||
console2.log("currentPrice", currentPrice); | ||
console2.log("currentPricePerShare", currentPricePerShare); | ||
|
||
while (currentCdp != bytes32(0)) { | ||
emit DebugBytes32(currentCdp); | ||
console2.log("CdpId", vm.toString(currentCdp)); | ||
console2.log("==============================="); | ||
console2.log("cdpManager.getCdpStake(currentCdp)", cdpManager.getCdpStake(currentCdp)); | ||
console2.log( | ||
"cdpManager.getSyncedCdpCollShares(currentCdp)", | ||
|
@@ -239,7 +250,16 @@ contract EToFoundry is | |
"cdpManager.getSyncedNominalICR(currentCdp)", | ||
cdpManager.getSyncedNominalICR(currentCdp) | ||
); | ||
console2.log( | ||
"cdpManager.getCachedICR(currentCdp, currentPrice)", | ||
cdpManager.getCachedICR(currentCdp, currentPrice) | ||
); | ||
console2.log( | ||
"cdpManager.getSyncedICR(currentCdp, currentPrice)", | ||
cdpManager.getSyncedICR(currentCdp, currentPrice) | ||
); | ||
currentCdp = sortedCdps.getNext(currentCdp); | ||
console2.log(""); | ||
} | ||
|
||
console2.log( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,12 @@ import "../contracts/Interfaces/IERC3156FlashLender.sol"; | |
* FlashLoan ReEntrancy Attack | ||
*/ | ||
|
||
interface IActivePool { | ||
function feeRecipientAddress() external view returns (address); | ||
|
||
function feeBps() external view returns (uint256); | ||
} | ||
|
||
contract FlashAttack { | ||
IERC20 public immutable want; | ||
IERC3156FlashLender public immutable lender; | ||
|
@@ -45,6 +51,37 @@ contract FlashAttack { | |
} | ||
} | ||
|
||
contract FlashFeeEscapeBorrower { | ||
IERC20 public immutable want; | ||
IERC3156FlashLender public immutable lender; | ||
uint256 public counter; | ||
|
||
constructor(IERC20 _want, IERC3156FlashLender _lender, uint256 _amt) { | ||
want = _want; | ||
lender = _lender; | ||
|
||
// Approve to repay | ||
IERC20(_want).approve(address(_lender), type(uint256).max); | ||
counter = _amt; | ||
} | ||
|
||
function onFlashLoan( | ||
address initiator, | ||
address token, | ||
uint256 amount, | ||
uint256 fee, | ||
bytes calldata data | ||
) external returns (bytes32) { | ||
require(token == address(want)); | ||
|
||
if (IERC20(want).balanceOf(address(this)) < counter) { | ||
lender.flashLoan(IERC3156FlashBorrower(address(this)), address(want), amount, data); | ||
} | ||
|
||
return keccak256("ERC3156FlashBorrower.onFlashLoan"); | ||
} | ||
} | ||
Comment on lines
+54
to
+83
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. The Consider adding reentrancy guards to the |
||
|
||
contract FlashLoanAttack is eBTCBaseFixture { | ||
function setUp() public override { | ||
// Base setup | ||
|
@@ -160,4 +197,33 @@ contract FlashLoanAttack is eBTCBaseFixture { | |
abi.encodePacked(uint256(0)) | ||
); | ||
} | ||
|
||
function testFeeEscapeAttack() public { | ||
uint256 _feeBps = IActivePool(address(activePool)).feeBps(); | ||
uint256 amount = 10000 / IActivePool(address(activePool)).feeBps(); | ||
uint256 totalAmount = amount * _feeBps; | ||
|
||
uint256 fee = activePool.flashFee(address(collateral), amount); | ||
|
||
vm.assume(fee == 0); | ||
|
||
FlashFeeEscapeBorrower attacker = new FlashFeeEscapeBorrower( | ||
IERC20(address(collateral)), | ||
IERC3156FlashLender(address(activePool)), | ||
amount | ||
); | ||
|
||
dealCollateral(address(activePool), totalAmount * 2); | ||
|
||
address _feeRecipient = IActivePool(address(activePool)).feeRecipientAddress(); | ||
uint256 _feeBefore = IERC20(address(collateral)).balanceOf(_feeRecipient); | ||
activePool.flashLoan( | ||
IERC3156FlashBorrower(address(attacker)), | ||
address(collateral), | ||
amount, | ||
abi.encodePacked(amount) | ||
); | ||
uint256 _feeAfter = IERC20(address(collateral)).balanceOf(_feeRecipient); | ||
require(_feeAfter == _feeBefore, "!flash fee should be zero due to division loss"); | ||
} | ||
} |
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.
The call to
CdpManager.liquidate
within theliquidate
function does not explicitly check for the success of the operation before proceeding with further logic. While thesuccess
variable is used later to conditionally execute code, it's crucial to handle the failure case immediately after the external call to prevent any unintended behavior. Consider adding a require statement to ensure that the liquidation call was successful before continuing.(success, returnData) = actor.proxy( address(cdpManager), abi.encodeWithSelector(CdpManager.liquidate.selector, _cdpId) ); + require(success, "Liquidation failed");
Committable suggestion