Summary
EmergencyMigration::migrate
function correctly upgrades the MoneyShelf
module to MoneyVault
, however, it fails to move the USDC tokens from the MoneyShelf
to the MoneyVault
. This omission can lead to a situation where the funds remain in the old contract, potentially exposed to the issues the migration was meant to address.
Vulnerability Details
EmergencyMigration::migrate
function is intended to transition control of USDC from the MoneyShelf
contract to the MoneyVault
contract in case of an emergency. However, the function only upgrades the module without transferring the actual USDC balance to the new contract. The relevant code snippet is as follows:
function migrate(Kernel kernel, IERC20 usdc, CrimeMoney crimeMoney) public returns (MoneyVault) {
vm.startBroadcast(kernel.executor());
MoneyVault moneyVault = new MoneyVault(kernel, usdc, crimeMoney);
@> kernel.executeAction(Actions.UpgradeModule, address(moneyVault));
vm.stopBroadcast();
return moneyVault;
}
Without transferring the USDC, the funds remain in the MoneyShelf
, defeating the purpose of the emergency migration.
Impact
USDC remains in the vulnerable MoneyShelf
contract, which may be exposed to the security issue the migration was meant to mitigate. Also, not transferring USDC to MoneyVault
undermines its role as a secure fallback during emergencies, potentially leading to financial losses or operational failures.
Tools Used
Manual Inspection
Proof of Code
Place the following test into the EmergencyMigration.t.sol
:
function test_migrate() public {
assertEq(address(kernel.getModuleForKeycode(Keycode.wrap("MONEY"))), address(moneyShelf));
vm.startPrank(deployer.godFather());
usdc.approve(address(moneyShelf), type(uint256).max);
laundrette.depositTheCrimeMoneyInATM(deployer.godFather(), address(moneyShelf), 1_000_000e6);
vm.stopPrank();
EmergencyMigration migration = new EmergencyMigration();
uint256 balance_moneyShelf_before = usdc.balanceOf(address(moneyShelf));
MoneyVault moneyVault = migration.migrate(kernel, laundrette, usdc, crimeMoney);
uint256 balance_moneyShelf_after = usdc.balanceOf(address(moneyShelf));
uint256 balance_moneyVault_after = usdc.balanceOf(address(moneyVault));
console.log("~~~ Before ~~~");
console.log("moneyShelf - before: ", balance_moneyShelf_before);
console.log("moneyVault - before: ", 0);
console.log("~~~~~~~~~~~~~~");
console.log("~~~ After ~~~");
console.log("moneyShelf - after: ", balance_moneyShelf_after);
console.log("moneyVault - after: ", balance_moneyVault_after);
console.log("~~~~~~~~~~~~~~");
assertNotEq(address(moneyShelf), address(moneyVault));
assertEq(address(kernel.getModuleForKeycode(Keycode.wrap("MONEY"))), address(moneyVault));
assertEq(balance_moneyShelf_before, balance_moneyShelf_after);
}
Running this test yields the following output:
[PASS] test_migrate() (gas: 2338186)
~~~ Before ~~~
moneyShelf - before: 1000000000000
moneyVault - before: 0
~~~~~~~~~~~~~~
~~~ After ~~~
moneyShelf - after: 1000000000000
moneyVault - after: 0
~~~~~~~~~~~~~~
Recommendations
To resolve this issue, modify the EmergencyMigration::migrate
function to include the transfer of USDC from the MoneyShelf
to the MoneyVault
.
function deploy() public returns (Kernel, IERC20, CrimeMoney, WeaponShelf, MoneyShelf, Laundrette) {
// Code above stays the same...
kernel.grantRole(Role.wrap("moneyshelf"), address(moneyShelf));
+ kernel.grantRole(Role.wrap("gangmember"), address(godFather)); // Add godfather to the gang
// Code below stays the same...
}
-function migrate(Kernel kernel, IERC20 usdc, CrimeMoney crimeMoney) public returns (MoneyVault) {
+function migrate(Kernel kernel, Laundrette laundrette, IERC20 usdc, CrimeMoney crimeMoney) public returns (MoneyVault) {
vm.startBroadcast(kernel.executor());
MoneyVault moneyVault = new MoneyVault(kernel, usdc, crimeMoney);
+ MoneyShelf moneyShelf = MoneyShelf(address(kernel.getModuleForKeycode(Keycode.wrap("MONEY"))));
+ laundrette.withdrawMoney(address(moneyShelf), address(moneyVault), usdc.balanceOf(address(moneyShelf)));
kernel.executeAction(Actions.UpgradeModule, address(moneyVault));
vm.stopBroadcast();
return moneyVault;
}
Running the test again would yield the following outcome:
[FAIL. Reason: assertion failed: 1000000000000 != 0] test_migrate() (gas: 2464336)
~~~ Before ~~~
moneyShelf - before: 1000000000000
moneyVault - before: 0
~~~~~~~~~~~~~~
~~~ After ~~~
moneyShelf - after: 0
moneyVault - after: 1000000000000
~~~~~~~~~~~~~~