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

Discrepancy in the decimal places of `CrimeMoney` and `USDC` - peg is not 1:1

Summary

CrimeMoney and USDC tokens have different decimal precision which in not taken into account in the protocol.
Consequently, CrimeMoney is not pegged 1:1 to USDC.

Vulnerability Details

Functions MoneyShelf::depositUSDC, MoneyShelf::withdrawUSDC and MoneyVault::withdrawUSDC mint and burn CrimeMoney like it had the same decimal precision as USDC, see e.g.

function depositUSDC(address account, address to, uint256 amount) external {
deposit(to, amount);
usdc.transferFrom(account, address(this), amount);
crimeMoney.mint(to, amount);
}

Here, the same amount (amount) is used in the deposit function and in the call to crimeMoney.mint.

However, in reality USDC has 6 decimal places, whereas CrimeMoney has 18.

The following test demonstrates that CrimeMoney is minted as it had the same decimal precision as the USDC token. Insert this test into Laundrette.t.sol and import the indicated dependencies:

Proof of Code
// @notice add: import { RealMockUSDC } from "./mocks/RealMockUSDC.sol";
// @notice add: import { MoneyShelf } from "test/Base.t.sol";
function testBrokenPeg() public {
///////////////////
///// SETUP ///////
///////////////////
// STEP 1: deploy the decimal-correct usdc contract
address centre = makeAddr("centre");
vm.prank(centre);
RealMockUSDC rusdc = new RealMockUSDC();
uint256 initialRealUsdcBalance = 1_000_000e6; // from the mock contract
// STEP 2: redeploy MoneyShelf with the correct USDC == realUsdc
MoneyShelf realMoneyShelf = new MoneyShelf(kernel, rusdc, crimeMoney);
// STEP 3: replace MoneyShelf module with realMoneyShelf module
vm.prank(godFather);
kernel.executeAction(Actions.UpgradeModule, address(realMoneyShelf));
// STEP 4: deactivate then re-activate the policy
// neccessary due to another bug, see
// "Omitted policy reconfig during module upgrade due to misassigned dependencies..."
vm.startPrank(godFather);
kernel.executeAction(Actions.DeactivatePolicy, address(laundrette));
kernel.executeAction(Actions.ActivatePolicy, address(laundrette));
vm.stopPrank();
// STEP 5: grantRole to realMoneyShelf, necessary for mint and burn
vm.prank(kernel.admin()); //
kernel.grantRole(Role.wrap("moneyshelf"), address(realMoneyShelf));
// STEP 6: Godfather acquires USDC
vm.prank(centre);
rusdc.transfer(godFather, initialRealUsdcBalance);
// STEP 7: initial balance checks
uint256 crimeMoneyBalance;
assertEq(rusdc.balanceOf(godFather), initialRealUsdcBalance);
assert(crimeMoney.balanceOf(godFather) == 0);
///////////////////
//// EXECUTION ////
///////////////////
vm.startPrank(godFather);
// STEP 1: Godfather deposits all his USDC
rusdc.approve(address(realMoneyShelf), initialRealUsdcBalance);
laundrette.depositTheCrimeMoneyInATM(godFather, godFather, initialRealUsdcBalance);
vm.stopPrank();
assertEq(rusdc.balanceOf(godFather), 0);
assertEq(realMoneyShelf.getAccountAmount(godFather), initialRealUsdcBalance);
///////////////////
/// ASSERTIONS ////
///////////////////
assert(rusdc.decimals() == 6); // 1 000 000 USDC == 1 USD
assert(crimeMoney.decimals() == 18); // 1 000 000 crimeMoney == 1e-12 USD
// total supply of the 2 tokens are the same
// but considering the different decimal places, it should not be the same
assert(rusdc.totalSupply() == crimeMoney.totalSupply());
}
Mock USDC contract with 6 decimal places
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { console } from "lib/forge-std/src/console.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
error AddressBlacklisted(address account);
contract RealMockUSDC is ERC20 {
address public immutable i_owner;
mapping(address => bool) private _blacklist;
////////////////////////////////////////////
/////////////// Modifiers //////////////////
////////////////////////////////////////////
modifier onlyOwner() {
require(msg.sender == i_owner, "Not the owner!");
_;
}
modifier notBlacklisted(address account) {
if (_blacklist[account]) {
revert AddressBlacklisted(account);
}
_;
}
////////////////////////////////////////////
/////////////// Events /////////////////////
////////////////////////////////////////////
event Blacklisted(address indexed account);
event Whitelisted(address indexed account);
////////////////////////////////////////////
/////////////// Functions //////////////////
////////////////////////////////////////////
constructor() ERC20("RUSDC", "RUSDC") {
i_owner = msg.sender;
_mint(msg.sender, 1_000_000e6);
}
function decimals() public view override returns (uint8) {
return 6;
}
////////////////////////////////////////////
/////// Blacklisting functionality /////////
////////////////////////////////////////////
function addToBlacklist(address account) external onlyOwner {
_blacklist[account] = true;
emit Blacklisted(account);
}
function removeFromBlacklist(address account) external onlyOwner {
_blacklist[account] = false;
emit Whitelisted(account);
}
////////////////////////////////////////////
/////// Transfer functions /////////////////
////////////////////////////////////////////
function transfer(
address recipient,
uint256 amount
)
public
override
notBlacklisted(msg.sender)
notBlacklisted(recipient)
returns (bool)
{
return super.transfer(recipient, amount);
}
function transferFrom(
address sender,
address recipient,
uint256 amount
)
public
override
notBlacklisted(sender)
notBlacklisted(recipient)
returns (bool)
{
return super.transferFrom(sender, recipient, amount);
}
}

Impact

In the scope of the protocol, there is no real issue: gangmembers will be able to withdraw the same amount of USDC as they depositied.

However, protocols and businesses that integrate with the protocol expecting a stable 1:1 peg might find their integrations unreliable. For instance, a payment gateway that accepts CrimeMoney expecting it to be equivalent to USDC token might face significant issues in transaction processing and value reconciliation.

Tools Used

Manual review, Foundry.

Recommendations

Consider reserving the same amount of decimals for MoneyShelf as USDC does, i.e.:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { Kernel, Actions, Role } from "./Kernel.sol";
contract CrimeMoney is ERC20 {
Kernel public kernel;
constructor(Kernel _kernel) ERC20("CrimeMoney", "CRIME") {
kernel = _kernel;
}
+ function decimals() public view override returns (uint8) {
+ return 6;
+ }
modifier onlyMoneyShelf() {
require(kernel.hasRole(msg.sender, Role.wrap("moneyshelf")), "CrimeMoney: only MoneyShelf can mint");
_;
}
function mint(address to, uint256 amount) public onlyMoneyShelf {
_mint(to, amount);
}
function burn(address from, uint256 amount) public onlyMoneyShelf {
_burn(from, amount);
}
}
Updates

Lead Judging Commences

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

USDC decimals not handled

Support

FAQs

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