Understanding MakerDAO's DssFlash Contract: A Complete Breakdown of the Flash Mint Module

In my free time iwrite and raise my voice in toast.
A deep dive into every line of MakerDAO's 183-line flash loan contract — what it does, how it works, why every design decision was made, and the broader system it lives in.
In January 2020, Aave introduced flash loans to DeFi and changed the game forever. For the first time, anyone could borrow millions of dollars with zero collateral — as long as they returned it in the same transaction.
MakerDAO took that idea further. Instead of lending from a pool of deposited tokens, MakerDAO's DssFlash contract mints DAI from nothing, lends it out, and destroys it when it comes back. No pool. No fees. No limits beyond a governance-set ceiling.
This article dissects the entire contract. By the end, you'll understand every line, every design choice, and the MakerDAO infrastructure that makes it all work.
What This Contract Is
DssFlash is MakerDAO's Flash Mint Module. It was proposed as MIP25 (Maker Improvement Proposal 25) by Sam MacPherson in September 2020, ratified in November 2020, and deployed to Ethereum mainnet at address 0x60744434d6339a6B27d73d9Eda62b6F66a0a04FA.
It is 183 lines of Solidity that allow anyone to borrow any amount of DAI — up to a configurable ceiling — with zero collateral, zero credit checks, and zero fees. The only requirement: you must return it within the same Ethereum transaction.
The contract implements two interfaces:
- ERC-3156 (
IERC3156FlashLender) — the Ethereum-wide flash loan standard - IVatDaiFlashLender — a MakerDAO-specific interface for internal accounting-level flash loans
You can read the source at github.com/makerdao/dss-flash, the ERC-3156 standard at eips.ethereum.org/EIPS/eip-3156, and the original proposal at MIP25.
The Problem It Solves
Why flash loans exist
In traditional finance, borrowing requires trust. Banks need credit checks, collateral, and income verification. Most people cannot borrow $10 million for a 30-second trade.
Blockchains have a property traditional finance doesn't: atomicity. An Ethereum transaction either succeeds completely or reverts completely. There is no partial state. If you borrow 10 million DAI and the transaction can't repay it, the entire thing reverts — the borrow never happened. The lender's funds were never at risk. No trust is needed.
Why MakerDAO's approach is different
Before DssFlash, flash loan liquidity came from pools. Aave lent tokens deposited by users. The available amount depended on how much was sitting idle in the pool.
MakerDAO doesn't lend from a pool. It mints DAI from nothing at the start of the flash loan and destroys it at the end. This creates four advantages:
Unlimited theoretical liquidity. Not limited by deposits — only by the governance-set ceiling.
Zero opportunity cost. The DAI doesn't come from anywhere, so no depositor is losing yield.
Zero fees. Since there's no depositor to compensate, MakerDAO charges nothing.
No fragmentation. Flash loan users don't compete with regular borrowers for liquidity.
This is called "flash minting" rather than "flash lending" because the tokens are created on demand.
Flash Loans: The Core Idea
A flash loan is a borrow-use-return cycle that completes within a single Ethereum transaction:
┌─────────────────────────────────────────────────────────────┐
│ ONE ETHEREUM TRANSACTION │
│ │
│ 1. Lender creates tokens and sends them to borrower │
│ 2. Lender calls borrower's callback function │
│ 3. Borrower does something (arbitrage, swap, liquidate) │
│ 4. Borrower approves lender to take tokens back │
│ 5. Lender pulls tokens back from borrower │
│ 6. Lender destroys the tokens │
│ │
│ If ANY step fails → everything reverts (nothing happened) │
└─────────────────────────────────────────────────────────────┘
The key mechanism is the callback pattern. The lender doesn't just send tokens and hope for the best. It calls a specific function on the borrower's contract (onFlashLoan), giving the borrower a chance to act. When that callback returns, the lender takes control back and verifies repayment.
What people use flash loans for
- Arbitrage — buy a token cheap on one exchange, sell it for more on another, keep the profit
- Collateral swaps — change the collateral backing a DeFi loan without closing the position
- Self-liquidation — repay a DeFi loan before getting liquidated (which has a penalty)
- Debt refinancing — move a loan from one protocol to another at better rates
The MakerDAO System You Need to Understand
You cannot understand DssFlash without understanding three MakerDAO concepts. I'll keep each one concise.
DAI exists in two forms
This is the most important concept.
Internal DAI lives inside the Vat contract (MakerDAO's core accounting engine). It's measured in RAD (10^45 precision). Only MakerDAO contracts can interact with it. It's used for protocol accounting — who owes what, who owns what.
External DAI is the ERC-20 token you see in wallets and on exchanges. It's measured in WAD (10^18 precision). Anyone can interact with it. This is the DAI you trade on Uniswap or hold in MetaMask.
Think of it like a casino. Internal DAI is casino chips — they only work inside the building. External DAI is cash — it works everywhere. A cashier's desk converts between the two.
That cashier's desk is called DaiJoin.
The Vat: MakerDAO's central ledger
The Vat (vat.sol) is the core contract of the entire MakerDAO system. It's a ledger that tracks all balances and debts.
DssFlash uses four Vat functions:
suck(sinAddr, daiAddr, amount) — Creates amount of DAI at daiAddr AND creates amount of debt (called "sin") at sinAddr. Money from nothing — but balanced by equal debt. Think of it as a bank writing a check to itself.
heal(amount) — The reverse. Destroys amount of DAI and amount of sin from the caller. Cancels the creation.
hope(address) — Authorizes another address to move your internal DAI. This is the Vat's version of ERC-20's approve().
move(from, to, amount) — Transfers internal DAI between accounts.
The suck + heal pair is the engine of flash minting. suck creates DAI and debt from thin air. heal destroys both. Net effect: zero.
The Vat also has a live() function that returns 1 when the system is running normally and 0 during emergency shutdown. DssFlash checks this before every flash loan.
DaiJoin: the bridge
DaiJoin converts between internal DAI and ERC-20 DAI.
exit(address, amount) converts internal DAI to ERC-20 DAI. It debits internal DAI from the caller and mints ERC-20 DAI to the specified address. This is "leaving the casino with cash."
join(address, amount) converts ERC-20 DAI to internal DAI. It burns the ERC-20 DAI and credits internal DAI to the specified address. This is "entering the casino and buying chips."
The unit system: WAD, RAY, RAD
Solidity has no decimal numbers. MakerDAO uses fixed-point arithmetic with three scales:
WAD (10^18 decimals) — Used for token amounts. When you see "1000 DAI," it's stored as 1000 × 10^18. This is the standard ERC-20 unit.
RAY (10^27 decimals) — Used for rates and ratios, like interest rates. Higher precision because rates need more decimal places.
RAD (10^45 decimals) — Used for Vat internal balances. This is WAD × RAY. When the Vat multiplies a token amount by a rate, the result naturally lands in RAD.
The critical relationship: WAD × RAY = RAD.
When DssFlash receives a flash loan amount in WAD from a user, it multiplies by RAY to get the RAD value needed for Vat operations:
uint256 amt = _mul(amount, RAY); // WAD × RAY = RAD
A concrete example: if someone requests a 1000 DAI flash loan, amount is 1000 × 10^18 (WAD), and amt becomes 1000 × 10^45 (RAD). That RAD value is what vat.suck() and vat.heal() use.
The Contract Architecture
IERC3156FlashLender IVatDaiFlashLender
(ERC-3156 standard) (MakerDAO custom)
│ │
└────────────┬───────────────┘
│
┌──────┴──────┐
│ DssFlash │
│ 183 lines │
└──────┬──────┘
│
┌────────────────┼────────────────┐
│ │ │
┌────┴────┐ ┌──────┴──────┐ ┌────┴────┐
│ Vat │ │ DaiJoin │ │ DAI │
│ (core │ │ (bridge) │ │ (ERC-20)│
│ ledger) │ │ │ │ token) │
└─────────┘ └─────────────┘ └─────────┘
immutable immutable immutable
DssFlash holds immutable references to three core MakerDAO contracts, all derived from a single constructor argument (daiJoin). It implements two interfaces with four public functions total.
What DssFlash owns: Nothing. It holds no tokens permanently. It mints DAI, lends it, gets it back, and burns it — all within one function call.
What DssFlash controls: A configurable ceiling (max) that limits how much can be flash-minted, and an authorization list (wards) that controls who can change that ceiling.
Section-by-Section Breakdown
The inline interfaces (Lines 23–43)
interface DaiLike {
function balanceOf(address) external returns (uint256);
function transferFrom(address, address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
}
interface DaiJoinLike {
function dai() external view returns (address);
function vat() external view returns (address);
function join(address, uint256) external;
function exit(address, uint256) external;
}
interface VatLike {
function hope(address) external;
function dai(address) external view returns (uint256);
function live() external view returns (uint256);
function move(address, address, uint256) external;
function heal(uint256) external;
function suck(address, address, uint256) external;
}
These are minimal interfaces — they only declare the functions DssFlash actually calls. The real Dai, DaiJoin, and Vat contracts have many more functions, but DssFlash doesn't need them.
MakerDAO calls these "Like" interfaces (DaiLike = "something that looks like Dai"). This convention appears throughout the MakerDAO codebase. The benefit: less code to compile and audit, and a clear signal of exactly what the contract depends on.
Notice that parameter names are omitted (just address instead of address owner). This is valid Solidity and is MakerDAO's compact style.
Contract declaration (Line 45)
contract DssFlash is IERC3156FlashLender, IVatDaiFlashLender {
Multiple inheritance. The contract must provide concrete implementations of maxFlashLoan, flashFee, flashLoan (from IERC3156FlashLender), and vatDaiFlashLoan (from IVatDaiFlashLender).
The authorization system (Lines 47–54)
function rely(address usr) external auth { wards[usr] = 1; emit Rely(usr); }
function deny(address usr) external auth { wards[usr] = 0; emit Deny(usr); }
mapping (address => uint256) public wards;
modifier auth {
require(wards[msg.sender] == 1, "DssFlash/not-authorized");
_;
}
This is MakerDAO's standard access control pattern, used across nearly every MakerDAO contract.
wards is a mapping from address to uint256. A value of 1 means authorized. A value of 0 (the default) means not authorized.
rely(usr) grants authorization. deny(usr) revokes it. Both require the caller to already be authorized (the auth modifier creates a chain of trust).
auth is a modifier that checks wards[msg.sender] == 1 before allowing the function to execute.
Why uint256 instead of bool? The EVM operates natively on 256-bit words. A bool is stored as a uint8 internally but still occupies a full 32-byte storage slot. Using uint256 avoids the overhead of type conversion. In Solidity 0.6, this was a meaningful gas optimization.
Who gets authorized first? The constructor sets wards[msg.sender] = 1 — the deployer. From there, the deployer can rely() other addresses (like MakerDAO governance contracts), which can manage authorization further. In practice, MakerDAO governance is the authorized entity.
State variables (Lines 56–65)
VatLike public immutable vat;
DaiJoinLike public immutable daiJoin;
DaiLike public immutable dai;
uint256 public max; // Maximum borrowable Dai [wad]
uint256 private locked; // Reentrancy guard
bytes32 public constant CALLBACK_SUCCESS =
keccak256("ERC3156FlashBorrower.onFlashLoan");
bytes32 public constant CALLBACK_SUCCESS_VAT_DAI =
keccak256("VatDaiFlashBorrower.onVatDaiFlashLoan");
Three immutable contract references — vat, daiJoin, and dai are set once in the constructor and embedded in the contract's bytecode. They can never be changed. Reading an immutable costs ~3 gas (a PUSH from bytecode) versus ~2,100 gas for a regular storage read (SLOAD). Since these are read on every flash loan, the savings compound.
max is the flash loan ceiling in WAD. It's the only configurable parameter, changed via file("max", newValue).
locked is the reentrancy guard flag. It's private because no external contract needs to see it.
Two constant callback hashes — these are the magic return values that borrower callbacks must produce. constant means they're computed at compile time (zero runtime cost). They're public so borrowers can reference them.
Events (Lines 67–72)
event Rely(address indexed usr);
event Deny(address indexed usr);
event File(bytes32 indexed what, uint256 data);
event FlashLoan(address indexed receiver, address token, uint256 amount, uint256 fee);
event VatDaiFlashLoan(address indexed receiver, uint256 amount, uint256 fee);
Events are logged to the blockchain's receipt trie. They're cheap to write but cannot be read by smart contracts — they're for off-chain systems like block explorers, analytics dashboards, and monitoring bots.
The indexed keyword makes parameters searchable. You can query "show me all flash loans where receiver = 0xABC" efficiently.
The fee parameter is always 0 in this implementation but is included for forward compatibility and standardized log parsing.
The reentrancy guard (Lines 74–79)
modifier lock {
require(locked == 0, "DssFlash/reentrancy-guard");
locked = 1;
_;
locked = 0;
}
This is a mutex (mutual exclusion lock). It prevents a function from being called while a previous call is still running.
Why this matters for flash loans: During a flash loan, the contract calls an external contract (the borrower's callback). That external contract could try to call flashLoan() again — a nested flash loan. Without the lock, a borrower could mint unlimited DAI in a recursive loop.
Both flashLoan() and vatDaiFlashLoan() share the same locked variable. This means you also can't mix the two — you can't call vatDaiFlashLoan inside a flashLoan callback, or vice versa.
The constructor (Lines 81–92)
constructor(address daiJoin_) public {
wards[msg.sender] = 1;
emit Rely(msg.sender);
VatLike vat_ = vat = VatLike(DaiJoinLike(daiJoin_).vat());
daiJoin = DaiJoinLike(daiJoin_);
DaiLike dai_ = dai = DaiLike(DaiJoinLike(daiJoin_).dai());
vat_.hope(daiJoin_);
dai_.approve(daiJoin_, type(uint256).max);
}
The constructor takes a single address — daiJoin_ — and derives everything else from it.
wards[msg.sender] = 1 — The deployer becomes the first authorized address. This bootstraps the chain of trust.
VatLike vat_ = vat = VatLike(DaiJoinLike(daiJoin_).vat()) — A chain of calls. It casts daiJoin_ to the DaiJoinLike interface, calls .vat() to get the Vat address, casts it to VatLike, and stores the result. The local variable vat_ is a gas optimization — reading an immutable inside the constructor is more expensive than a local variable because the immutable value hasn't been embedded in bytecode yet.
vat_.hope(daiJoin_) — Pre-authorizes DaiJoin to move DssFlash's internal Vat DAI. Without this, daiJoin.exit() would fail.
dai_.approve(daiJoin_, type(uint256).max) — Infinite ERC-20 approval to DaiJoin. This lets DaiJoin burn DssFlash's ERC-20 DAI during daiJoin.join(). Doing it once in the constructor avoids paying gas for an approval on every single flash loan.
Design insight: Requiring only one constructor argument and deriving the rest is elegant. It reduces deployment errors (you can't accidentally pass mismatched Vat and DaiJoin addresses) and makes the contract self-documenting about its dependencies.
Math (Lines 94–99)
uint256 constant RAY = 10 ** 27;
uint256 constant RAD = 10 ** 45;
function _mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
require(y == 0 || (z = x * y) / y == x);
}
RAY and RAD are conversion constants. WAD (10^18) is implied since it's the standard ERC-20 unit.
_mul is a safe multiplication function. Solidity 0.6 does NOT check for overflow. If two large numbers multiply and the result exceeds 2^256 - 1, it silently wraps around. This function detects that: compute z = x * y, then verify z / y == x. If the multiplication overflowed, the division check will fail and the transaction reverts.
Why only _mul? DssFlash only needs multiplication — converting WAD to RAD via _mul(amount, RAY). There's no addition or subtraction of different-unit values, so _add and _sub aren't needed. Minimal code = smaller attack surface.
Administration (Lines 101–109)
function file(bytes32 what, uint256 data) external auth {
if (what == "max") {
require((max = data) <= RAD, "DssFlash/ceiling-too-high");
}
else revert("DssFlash/file-unrecognized-param");
emit File(what, data);
}
file() is MakerDAO's universal configuration pattern. Instead of separate setters (setMax(), setFee()), they use a single function with a bytes32 key. This reduces the contract surface area, makes governance easier (one function signature for all config changes), and is extensible.
Why bytes32 instead of string? Comparing bytes32 values is a single EVM operation. Comparing strings requires looping through characters. Gas matters.
require((max = data) <= RAD) is a compact expression that assigns data to max and simultaneously checks the new value is ≤ RAD (10^45). The RAD ceiling prevents overflow: since max is in WAD and gets multiplied by RAY inside flashLoan(), a value larger than RAD could overflow in the multiplication. In practice, RAD means ~10^27 DAI — an astronomically large number far beyond what governance would ever set.
maxFlashLoan (Lines 112–120)
function maxFlashLoan(
address token
) external override view returns (uint256) {
if (token == address(dai) && locked == 0) {
return max;
} else {
return 0;
}
}
Returns the maximum available flash loan amount. Per ERC-3156: return the ceiling for supported tokens (only DAI) and 0 for unsupported tokens.
The locked == 0 check is subtle: during an active flash loan, locked is 1, so this returns 0. This tells callers "no flash loans available right now" — accurate, since reentrancy is blocked.
flashFee (Lines 122–130)
function flashFee(
address token,
uint256 amount
) external override view returns (uint256) {
amount;
require(token == address(dai), "DssFlash/token-unsupported");
return 0;
}
MakerDAO charges zero fees.
The lone amount; statement silences the compiler's "unused variable" warning. The parameter exists because ERC-3156 requires it — other implementations might charge percentage-based fees.
Per ERC-3156, flashFee reverts for unsupported tokens (unlike maxFlashLoan, which returns 0). The asymmetry is intentional: calling flashFee with an unsupported token is a programming error, while querying maxFlashLoan for an unsupported token is a valid question with a valid answer ("zero").
flashLoan: The Heart of the Contract (Lines 132–159)
function flashLoan(
IERC3156FlashBorrower receiver,
address token,
uint256 amount,
bytes calldata data
) external override lock returns (bool) {
require(token == address(dai), "DssFlash/token-unsupported");
require(amount <= max, "DssFlash/ceiling-exceeded");
require(vat.live() == 1, "DssFlash/vat-not-live");
uint256 amt = _mul(amount, RAY);
vat.suck(address(this), address(this), amt);
daiJoin.exit(address(receiver), amount);
emit FlashLoan(address(receiver), token, amount, 0);
require(
receiver.onFlashLoan(msg.sender, token, amount, 0, data)
== CALLBACK_SUCCESS,
"DssFlash/callback-failed"
);
dai.transferFrom(address(receiver), address(this), amount);
daiJoin.join(address(this), amount);
vat.heal(amt);
return true;
}
This is the core of the entire contract. Let me trace through every step.
Step 0: Validation and locking
The lock modifier fires first — checks that locked == 0, then sets it to 1. Three require checks follow: the token must be DAI, the amount must be within the ceiling, and the MakerDAO system must be live (not in emergency shutdown).
Step 1: Convert units
uint256 amt = _mul(amount, RAY);
Convert amount from WAD (10^18, what the user passed) to RAD (10^45, what the Vat uses). For a 1000 DAI loan: 1000 × 10^18 × 10^27 = 1000 × 10^45.
Step 2: Create DAI in the Vat
vat.suck(address(this), address(this), amt);
Create DAI from nothing. Both parameters are address(this) (DssFlash itself):
- First parameter: where to record the debt (sin) → DssFlash
- Second parameter: where to credit the DAI → DssFlash
DssFlash now has amt of internal DAI AND amt of debt. They balance each other.
Step 3: Convert to ERC-20 and send to borrower
daiJoin.exit(address(receiver), amount);
DaiJoin debits internal DAI from DssFlash's Vat balance and mints ERC-20 DAI tokens to the borrower. The borrower now has real, spendable DAI.
Step 4: Emit the event
emit FlashLoan(address(receiver), token, amount, 0);
Log it. Fee is 0.
Step 5: Call the borrower's callback
require(
receiver.onFlashLoan(msg.sender, token, amount, 0, data)
== CALLBACK_SUCCESS,
"DssFlash/callback-failed"
);
This is where control passes to the borrower. The borrower receives five parameters: the initiator (msg.sender — who called flashLoan), the token address, the amount, the fee (0), and arbitrary data passed through from the original call.
Inside this callback, the borrower:
- Does their thing — arbitrage, collateral swap, whatever
- Must call
dai.approve(address(flash), amount)so DssFlash can pull the DAI back - Must return the exact
CALLBACK_SUCCESShash (keccak256("ERC3156FlashBorrower.onFlashLoan"))
If the return value doesn't match, the entire transaction reverts.
Step 6: Pull DAI back from borrower
dai.transferFrom(address(receiver), address(this), amount);
DssFlash uses the ERC-20 transferFrom to pull the DAI back. This relies on the borrower having called approve() during the callback. If they didn't approve, or they don't have enough DAI, this reverts — and with it, the entire transaction.
Step 7: Convert back to internal DAI
daiJoin.join(address(this), amount);
DaiJoin burns the ERC-20 DAI and credits internal DAI back to DssFlash's Vat account.
Step 8: Destroy DAI and cancel debt
vat.heal(amt);
heal() cancels amt of DAI against amt of sin (debt). Both go to zero. The Vat is back to its pre-flash-loan state. Net effect: nothing.
Step 9: Cleanup
The lock modifier sets locked back to 0. The function returns true.
A note on initiator vs. receiver
The msg.sender of the flashLoan() call is the "initiator." The receiver gets the tokens and the callback. They can be different addresses. Alice (initiator) might call flashLoan() with Bob's contract as the receiver. The initiator parameter in the callback lets the receiver verify who started the loan — useful for access control.
vatDaiFlashLoan: The Fast Path (Lines 161–182)
function vatDaiFlashLoan(
IVatDaiFlashBorrower receiver,
uint256 amount, // [rad]
bytes calldata data
) external override lock returns (bool) {
require(amount <= _mul(max, RAY), "DssFlash/ceiling-exceeded");
require(vat.live() == 1, "DssFlash/vat-not-live");
vat.suck(address(this), address(receiver), amount);
emit VatDaiFlashLoan(address(receiver), amount, 0);
require(
receiver.onVatDaiFlashLoan(msg.sender, amount, 0, data)
== CALLBACK_SUCCESS_VAT_DAI,
"DssFlash/callback-failed"
);
vat.heal(amount);
return true;
}
This is the Vat-level flash loan — a faster, cheaper alternative for contracts that operate inside MakerDAO's accounting system.
How it differs from flashLoan
Amount unit. flashLoan takes WAD (10^18). vatDaiFlashLoan takes RAD (10^45).
Token form. flashLoan gives you ERC-20 DAI tokens. vatDaiFlashLoan gives you internal Vat DAI.
Number of steps. flashLoan has 6 steps (suck, exit, callback, transferFrom, join, heal). vatDaiFlashLoan has 3 steps (suck, callback, heal). No DaiJoin conversions needed.
Gas cost. flashLoan costs ~170K+ gas in overhead. vatDaiFlashLoan costs ~45K. Significantly cheaper.
Who can use it. flashLoan is for any contract. vatDaiFlashLoan is for contracts that understand MakerDAO's Vat internals.
Repayment method. flashLoan borrowers approve an ERC-20 transfer. vatDaiFlashLoan borrowers call vat.move() to send internal DAI back.
Key details
Ceiling check: require(amount <= _mul(max, RAY)) — since amount is in RAD and max is in WAD, it converts max to RAD for comparison.
Suck destination: Unlike flashLoan which sucks to itself then exits via DaiJoin, vatDaiFlashLoan sucks directly to the receiver: vat.suck(address(this), address(receiver), amount). Debt goes to DssFlash. DAI goes straight to the receiver. No middleman.
Repayment expectation: The borrower must call vat.move(address(this), address(flash), amount) inside the callback to return the internal DAI. If they don't, vat.heal(amount) will fail because DssFlash won't have enough internal DAI to cancel against its debt, and the whole transaction reverts.
The Two Modes Side by Side
ERC-3156 mode (flashLoan) — for external users
You want ERC-20 DAI tokens you can use on Uniswap, Aave, Compound, or any DeFi protocol.
User → DssFlash → Vat (suck) → DaiJoin (exit) → User gets ERC-20 DAI
↓
User does stuff
↓
User's DAI → DssFlash → DaiJoin (join) → Vat (heal) → Cleaned up
Vat DAI mode (vatDaiFlashLoan) — for MakerDAO-native contracts
You're doing something that stays inside MakerDAO's system — managing vault positions, adjusting collateral, interacting with the liquidation engine. No ERC-20 tokens needed.
User → DssFlash → Vat (suck directly to user) → User does Vat stuff
↓
User moves DAI back
↓
DssFlash → Vat (heal) → Cleaned up
The Security Model: Seven Layers
Layer 1: Reentrancy guard
The lock modifier prevents nested flash loans. A single locked variable covers both flashLoan and vatDaiFlashLoan, blocking cross-function reentrancy too.
Layer 2: Authorization
Only authorized addresses can change configuration. Anyone can call flashLoan and vatDaiFlashLoan. Only authorized addresses (governance) can call rely, deny, and file.
Layer 3: Callback verification
The borrower's callback must return an exact keccak256 hash. This prevents contracts without a proper callback from accidentally "succeeding," and ensures the borrower is intentionally participating in the flash loan.
Layer 4: Amount ceiling
Governance sets a maximum flash loan amount. Even though the DAI is atomically created and destroyed, a massive flash loan could theoretically manipulate markets or governance during the transaction. The ceiling limits exposure.
Layer 5: System liveness check
If MakerDAO enters emergency shutdown, all flash loans are disabled. require(vat.live() == 1) prevents flash loans from interfering with the shutdown process.
Layer 6: Immutable core addresses
The vat, daiJoin, and dai addresses are immutable. Even a compromised governance system cannot redirect flash loans to malicious contracts. This eliminates an entire class of upgrade-related attacks.
Layer 7: Overflow protection
The _mul function prevents arithmetic overflow that could create incorrect RAD amounts, which could lead to more DAI being created than intended.
What DssFlash does NOT protect against
DssFlash gives anyone massive capital for one transaction. What they do with it is not DssFlash's concern. If someone uses a flash loan to manipulate an oracle, exploit a vulnerable protocol, or game a governance vote — that's a problem in the target protocol, not in DssFlash.
The Test Suite: What It Proves
The test file (flash.t.sol, 518 lines) creates a complete miniature MakerDAO system and validates every edge case using the ds-test framework.
The setup deploys a TestVat, Spotter (oracle), TestVow (debt tracker), a gold token for collateral, the full DaiJoin system, DssFlash itself, and nine different receiver contracts — each testing a different scenario.
The nine test receivers
TestDoNothingReceiver — Returns the success hash but doesn't repay. Proves that not repaying reverts the transaction.
TestImmediatePaybackReceiver — The happy path. Approves repayment and returns success.
TestLoanAndPaybackReceiver — Mints extra DAI during the callback, then repays. Tests that callbacks can do complex multi-step operations.
TestLoanAndPaybackAllReceiver — Repays more than borrowed. Tests overpayment.
TestLoanAndPaybackDataReceiver — Decodes the data parameter to determine behavior. Tests that arbitrary data passes through correctly.
TestReentrancyReceiver — Tries to call flashLoan again inside the callback. Proves the reentrancy guard works.
TestDEXTradeReceiver — Simulates a realistic scenario: burns DAI, mints gold, deposits gold as collateral, borrows DAI to repay. This is the closest thing to a real arbitrage test.
TestBadReturn — Returns the wrong keccak256 hash. Proves callback verification works.
TestNoCallbacks — An empty contract with no callback function at all. Proves that non-implementing contracts fail.
Key test cases
test_mint_payback — Basic happy path for both ERC-3156 and Vat DAI modes. Verifies all balances return to zero.
testFail_flash_vat_not_live — Flash loans are disabled during emergency shutdown.
test_mint_zero_amount — Borrowing zero is allowed (edge case handling).
testFail_mint_amount_over_line — Exceeding the ceiling reverts.
testFail_mint_line_zero — Setting the ceiling to 0 effectively pauses all flash loans.
testFail_mint_unauthorized_suck — If DssFlash isn't authorized on the Vat, suck fails.
testFail_mint_reentrancy — Nested flash loans revert.
test_dex_trade — A realistic arbitrage scenario works end-to-end.
testFail_line_limit — Ceiling above RAD (10^45) is rejected.
testFail_bad_return_hash — Wrong callback return value reverts.
testFail_no_callbacks — Contracts without the callback interface fail.
The testFail_ prefix is a ds-test convention: these tests are expected to revert. The test passes if the function reverts, and fails if it doesn't.
The Supporting Cast
FlashLoanReceiverBase
An abstract helper contract at src/base/FlashLoanReceiverBase.sol that makes building flash loan borrowers easier:
abstract contract FlashLoanReceiverBase
is IVatDaiFlashBorrower, IERC3156FlashBorrower
{
DssFlash public flash;
bytes32 public constant CALLBACK_SUCCESS =
keccak256("ERC3156FlashBorrower.onFlashLoan");
bytes32 public constant CALLBACK_SUCCESS_VAT_DAI =
keccak256("VatDaiFlashBorrower.onVatDaiFlashLoan");
constructor(address _flash) public {
flash = DssFlash(_flash);
}
function approvePayback(uint256 amount) internal {
flash.dai().approve(address(flash), amount);
}
function payBackVatDai(uint256 amount) internal {
flash.vat().move(address(this), address(flash), amount);
}
}
It provides a stored reference to DssFlash, the two magic constant hashes, approvePayback() (handles the ERC-20 approval for repayment), payBackVatDai() (handles the vat.move for Vat DAI repayment), and math helpers (add, mul, rad).
Inherit from this base and you only need to implement onFlashLoan and onVatDaiFlashLoan. The repayment plumbing is handled for you.
The four interface files
IERC3156FlashLender.sol defines maxFlashLoan, flashFee, and flashLoan.
IERC3156FlashBorrower.sol defines onFlashLoan.
IVatDaiFlashLender.sol defines vatDaiFlashLoan.
IVatDaiFlashBorrower.sol defines onVatDaiFlashLoan.
These are standard interface definitions with NatSpec documentation comments.
Real-World Context
Deployment
Mainnet: 0x60744434d6339a6B27d73d9Eda62b6F66a0a04FA
Goerli testnet: 0xAa5F7d5b29Fa366BB04F6E4c39ACF569d5214075
Compiler: Solidity 0.6.12
How the ceiling is governed
The max parameter is set by MakerDAO governance through their voting system. MKR token holders vote on proposals. When passed, the governance contract calls flash.file("max", newValue). The ceiling is a safety mechanism — even though flash-minted DAI is ephemeral, governance limits how much can exist during a single transaction.
How DssFlash compares to other flash loan providers
DssFlash (MakerDAO) mints DAI from nothing. Zero fees. ERC-3156 standard. Only supports DAI.
Aave lends from depositor pools. Charges 0.05–0.09% fee. Custom interface with ERC-3156 wrapper available. Supports multiple tokens.
dYdX lends from depositor pools. Effectively zero fees but complex mechanism. Custom interface. Multiple tokens.
Uniswap V2/V3 lends from liquidity pools via "flash swaps." Charges the ~0.3% swap fee. Custom callback interface. Any listed token pair.
DssFlash's advantage: unlimited liquidity (not constrained by pool size) and zero fees. Its limitation: DAI only.
Common Questions
"If DAI is created from nothing, doesn't that affect the DAI supply?"
No. The DAI is created and destroyed within the same transaction. Between blocks, the total supply is unchanged. The flash-minted DAI never appears in a block's final state — it exists only ephemerally during transaction execution.
"Can someone flash loan more DAI than currently exists?"
Yes, up to the ceiling. Since the DAI is minted from scratch, it doesn't depend on existing supply. Governance sets a reasonable ceiling to limit potential market impact during the transaction.
"Why is there no fee?"
MakerDAO earns revenue from stability fees (interest on regular DAI loans backed by collateral). Flash loans have zero risk to the protocol — if repayment fails, the transaction reverts and nothing happened. Charging a fee would only discourage usage without generating meaningful revenue. Free flash loans encourage developers to build on MakerDAO.
"Why does vat.suck put both DAI and sin at DssFlash's own address?"
Bookkeeping. The Vat requires balanced creation — every unit of DAI must be matched by a unit of sin (debt). By placing both at DssFlash's address, heal() can cancel them against each other at the end. The DAI moves to the borrower via DaiJoin during the loan, but the debt stays at DssFlash until healing.
"What if the borrower doesn't approve DssFlash in the callback?"
dai.transferFrom(address(receiver), address(this), amount) on line 154 will revert because DssFlash has insufficient allowance. Since this happens inside the same transaction, everything reverts — the DAI creation, the callback, all state changes. The Vat returns to its pre-transaction state.
"What if the borrower keeps the DAI and returns the correct hash?"
The transferFrom will still fail because the borrower spent their DAI and doesn't have enough to return. Even with the correct callback hash, the token pull reverts the entire transaction.
"Could a malicious contract fake the callback success?"
It could return CALLBACK_SUCCESS, but it doesn't help. The callback hash is just one check. The real protection is transferFrom — DssFlash physically takes the tokens back. A contract that returns the right hash but can't cover the transfer simply reverts.
"Can you flash loan during emergency shutdown?"
No. require(vat.live() == 1) on line 140 blocks all flash loans when the system is shut down.
Closing Thoughts
DssFlash is 183 lines of Solidity. In those 183 lines, it implements ERC-3156 compliance, a second Vat-native flash loan mode, MakerDAO's authorization pattern, a reentrancy guard, overflow-safe math, unit conversion between WAD and RAD, governance-configurable ceilings, emergency shutdown integration, event logging, and seven layers of security.
It holds no tokens. It has no pool. It creates money from nothing and destroys it before the transaction ends. The net effect on the MakerDAO system is always zero.
That's the elegance of flash minting: the boldest lending operation in DeFi is also, paradoxically, the safest.
References
- Contract Source Code: github.com/makerdao/dss-flash
- MIP25 Proposal: MIP25 on GitHub
- ERC-3156 Specification: eips.ethereum.org/EIPS/eip-3156
- MakerDAO Flash Mint Module Docs: docs.makerdao.com — Flash Mint Module
- MakerDAO Vat Documentation: docs.makerdao.com — Vat
- MakerDAO DaiJoin Documentation: docs.makerdao.com — Join
- MakerDAO System Glossary: docs.makerdao.com — Glossary
- DSS Unit System (WAD/RAY/RAD): github.com/makerdao/dss — DEVELOPING.md
- Solidity 0.6.12 Documentation: docs.soliditylang.org
- Chainlink Flash Loans Explainer: chain.link/education-hub/flash-loans


