Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: medium
Valid

Deposited USDC Stuck in `MoneyShelf` in Emergency Mode

[H-2] Deposited USDC Stuck in MoneyShelf in Emergency Mode

Description:
Note: This only happens if the bug in the Laundrette::configureDependencies is fixed.

function configureDependencies() external override onlyKernel returns (Keycode[] memory dependencies) {
dependencies = new Keycode[](2);
dependencies[0] = toKeycode("MONEY");
moneyShelf = MoneyShelf(getModuleAddress(toKeycode("MONEY")));
@> dependencies[1] = toKeycode("WEAPN"); // this is the previous bug we assume is corrected.
weaponShelf = WeaponShelf(getModuleAddress(toKeycode("WEAPN")));
}

Upon addressing the bug in configureDependencies, a new issue arises where the MoneyShelf module fails to transfer deposited USDC to the newly upgraded MoneyVault during emergency mode. Additionally, the bank mapping is not updated accordingly, leaving the funds trapped within MoneyShelf.

Impact:
Although this issue does not lead to permanent fund loss, and is fixable by just executing UpgradeModule and replacing the MoneyVault with MoneyShelf again. it severely impacts the functionality of Emergency Mode, rendering funds inaccessible to all actors involved.

Proof of Concept:
To prove the concept, add the following test to EmergencyMigration.t.sol, Exploit Steps:

  1. gangM1 joins gang.

  2. gangM1 deposits some USDC to test it after emergency.

  3. godfather enters emergency mode via calling migrate.

  4. gangM1 Can Not withdraw because in emergency mode and previous bug is fixed.

  5. godfather Can Not withdraw because the funds are in Moneyshell and the bank mapping is not updated.

function testMoenyIsStuckInMoneyShelf() public {
//gangM1 joins gang
address gangM1 = makeAddr("M1");
joinGang(gangM1);
vm.prank(godFather);
usdc.transfer(gangM1, 100e6);
//some deposits for contract to test it after emergency
vm.startPrank(gangM1);
usdc.approve(address(moneyShelf), 100e6);
laundrette.depositTheCrimeMoneyInATM(gangM1, gangM1, 100e6);
vm.stopPrank();
assertEq(usdc.balanceOf(gangM1), 0);
assertEq(usdc.balanceOf(address(moneyShelf)), 100e6);
assertEq(address(kernel.getModuleForKeycode(Keycode.wrap("MONEY"))), address(moneyShelf));
//enter emergency mode:
EmergencyMigration migration = new EmergencyMigration();
MoneyVault moneyVault = migration.migrate(kernel, usdc, crimeMoney);
assertNotEq(address(moneyShelf), address(moneyVault));
assertEq(address(kernel.getModuleForKeycode(Keycode.wrap("MONEY"))), address(moneyVault));
//gangM1 Can not withdraw in emergency mode:
vm.startPrank(gangM1);
vm.expectRevert("MoneyVault: only GodFather can receive USDC");
laundrette.withdrawMoney(gangM1, gangM1, 100e6);
//godfather Can NOT withdraw in emergency mode:
uint256 gfBalanceBefore = usdc.balanceOf(godFather);
vm.startPrank(godFather);
vm.expectRevert(); // will revert with: arithmetic underflow or overflow (0x11) because Shelf.withdraw()
laundrette.withdrawMoney(gangM1, godFather, 100e6);
assertEq(usdc.balanceOf(godFather) - gfBalanceBefore, 0); //godFather balance change is zero
assertEq(usdc.balanceOf(address(moneyShelf)), 100e6); // funds are still in moneyShelf
assertEq(usdc.balanceOf(address(moneyVault)), 0); // moenyvault is empty
}

Recommended Mitigation:
Introduce an emergencyMode function within MoneyShelf. This function should facilitate the transfer of funds to the new MoneyVault whenever there's a change in the MONEY keycode. Modify Laundrette to invoke this function when necessary. Furthermore, directly update the bank mapping within MoneyVault::withdrawUSDC to circumvent the issue.

Recommended Mitigation Steps

First, implement the emergencyMode function in MoneyShelf:

+ function emergencyMode(address _newVault) external permissioned {
+ usdc.transfer(_newVault, usdc.balanceOf(address(this)));
+ }

Next, adjust Laundrette::configureDependencies and Laundrette::requestPermissions to utilize this new function:

function configureDependencies() external override onlyKernel returns (Keycode[] memory dependencies) {
dependencies = new Keycode[](2);
dependencies[0] = toKeycode("MONEY");
+ if (address(moneyShelf) != address(0)) {
+ address MoneyVault = getModuleAddress(toKeycode("MONEY"));
+ moneyShelf.emergencyMode(MoneyVault);
+ }
moneyShelf = MoneyShelf(getModuleAddress(toKeycode("MONEY")));
dependencies[1] = toKeycode("WEAPN");
weaponShelf = WeaponShelf(getModuleAddress(toKeycode("WEAPN")));
}
function requestPermissions() external view override onlyKernel returns (Permissions[] memory requests) {
- requests = new Permissions[](4);
+ requests = new Permissions[](5);
requests[0] = Permissions(toKeycode("MONEY"), moneyShelf.depositUSDC.selector);
requests[1] = Permissions(toKeycode("MONEY"), moneyShelf.withdrawUSDC.selector);
requests[2] = Permissions(toKeycode("WEAPN"), weaponShelf.deposit.selector);
requests[3] = Permissions(toKeycode("WEAPN"), weaponShelf.withdraw.selector);
+ requests[4] = Permissions(toKeycode("MONEY"), moneyShelf.emergencyMode.selector);
}

Finally, modify MoneyVault::withdrawUSDC to bypass the need for updating the bank mapping and burning tokens through crimeMoney:

function withdrawUSDC(address account, address to, uint256 amount) external {
require(to == kernel.executor(), "MoneyVault: only GodFather can receive USDC");
- withdraw(account, amount);
- crimeMoney.burn(account, amount);
usdc.transfer(to, amount);
}

These adjustments ensure that the GodFather remains the sole authority for withdrawing funds during emergencies, but it will cause the loss of monetary value associated with crimeMoney.

Updates

Lead Judging Commences

n0kto Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Emergency migration leave the USDC

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.