Comparing base with maker

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/base/SKILL.md

Base L2 Development

Base is Coinbase's L2 built on Optimism's OP Stack. It shares Ethereum security, uses ETH as gas, and settles to L1. If you can deploy to Ethereum, you can deploy to Base with zero code changes — just point at a different RPC.

What You Probably Got Wrong

  • Base is NOT a separate EVM — It is an OP Stack rollup. Same opcodes, same Solidity compiler, same tooling. There is nothing "Base-specific" about contract development. If your contract works on Ethereum or Optimism, it works on Base.
  • Gas has two components — L2 execution gas (cheap, ~0.001 gwei base fee) plus L1 data fee (variable, depends on Ethereum blob fees). The L1 data fee is the dominant cost for most transactions. After EIP-4844 (Dencun), L1 data costs dropped ~100x.
  • msg.sender works differently with Smart Wallet — Coinbase Smart Wallet is a smart contract account (ERC-4337). msg.sender is the smart wallet address, not the EOA. If your contract checks tx.origin == msg.sender to block contracts, it will block Smart Wallet users.
  • OnchainKit is NOT a wallet library — It is a React component library for building Base-native UIs. It handles identity resolution, transaction submission, token swaps, and Farcaster frames. You still need wagmi/viem for the underlying wallet connection.
  • Paymaster does NOT mean free transactions — A paymaster sponsors gas on behalf of users. Someone still pays. You configure a Coinbase Developer Platform paymaster policy that defines which contracts and methods are sponsored, with spend limits.
  • Block time is 2 seconds — Not 12 seconds like Ethereum L1. Plan your polling and confirmation logic accordingly.
  • Basescan and Blockscout are both available — Basescan (Etherscan-based) is the primary explorer. Blockscout is the alternative. Contract verification works on both but uses different APIs.

Quick Start

# Add Base to Foundry project
forge create src/Counter.sol:Counter \
  --rpc-url https://mainnet.base.org \
  --private-key $PRIVATE_KEY

# Check balance
cast balance $ADDRESS --rpc-url https://mainnet.base.org

# Deploy to testnet
forge script script/Deploy.s.sol \
  --rpc-url https://sepolia.base.org \
  --broadcast --verify \
  --etherscan-api-key $BASESCAN_API_KEY
import { createPublicClient, createWalletClient, http } from 'viem';
import { base, baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

const walletClient = createWalletClient({
  chain: base,
  transport: http(),
  account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
});

const blockNumber = await publicClient.getBlockNumber();

Chain Configuration

Base Mainnet

ParameterValue
Chain ID8453
CurrencyETH
RPC (public)https://mainnet.base.org
RPC (Coinbase)https://api.developer.coinbase.com/rpc/v1/base/$CDP_API_KEY
WebSocketwss://mainnet.base.org
Explorerhttps://basescan.org
Blockscouthttps://base.blockscout.com
Bridgehttps://bridge.base.org
Block time2 seconds
Finality~12 minutes (L1 finality)

Base Sepolia (Testnet)

ParameterValue
Chain ID84532
CurrencyETH
RPC (public)https://sepolia.base.org
RPC (Coinbase)https://api.developer.coinbase.com/rpc/v1/base-sepolia/$CDP_API_KEY
Explorerhttps://sepolia.basescan.org
Faucethttps://www.coinbase.com/faucets/base-ethereum-goerli-faucet

Viem Chain Definitions

viem includes Base chains out of the box:

import { base, baseSepolia } from 'viem/chains';

// base.id === 8453
// baseSepolia.id === 84532

Hardhat Network Config

// hardhat.config.ts
const config: HardhatUserConfig = {
  networks: {
    base: {
      url: process.env.BASE_RPC_URL || 'https://mainnet.base.org',
      accounts: [process.env.PRIVATE_KEY!],
      chainId: 8453,
    },
    baseSepolia: {
      url: process.env.BASE_SEPOLIA_RPC_URL || 'https://sepolia.base.org',
      accounts: [process.env.PRIVATE_KEY!],
      chainId: 84532,
    },
  },
  etherscan: {
    apiKey: {
      base: process.env.BASESCAN_API_KEY!,
      baseSepolia: process.env.BASESCAN_API_KEY!,
    },
    customChains: [
      {
        network: 'base',
        chainId: 8453,
        urls: {
          apiURL: 'https://api.basescan.org/api',
          browserURL: 'https://basescan.org',
        },
      },
      {
        network: 'baseSepolia',
        chainId: 84532,
        urls: {
          apiURL: 'https://api-sepolia.basescan.org/api',
          browserURL: 'https://sepolia.basescan.org',
        },
      },
    ],
  },
};

Foundry Config

# foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]

[rpc_endpoints]
base = "https://mainnet.base.org"
base_sepolia = "https://sepolia.base.org"

[etherscan]
base = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api", chain = 8453 }
base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api", chain = 84532 }

Deployment

Base uses the OP Stack. Deployment is identical to Ethereum — no special compiler flags, no custom precompiles for basic use cases.

Foundry

# Deploy to Base Sepolia
forge script script/Deploy.s.sol:DeployScript \
  --rpc-url https://sepolia.base.org \
  --broadcast \
  --verify \
  --etherscan-api-key $BASESCAN_API_KEY

# Deploy to Base Mainnet
forge script script/Deploy.s.sol:DeployScript \
  --rpc-url https://mainnet.base.org \
  --broadcast \
  --verify \
  --etherscan-api-key $BASESCAN_API_KEY \
  --slow

Verification on Basescan

# Verify after deployment
forge verify-contract $CONTRACT_ADDRESS src/MyContract.sol:MyContract \
  --chain base \
  --etherscan-api-key $BASESCAN_API_KEY

# With constructor args
forge verify-contract $CONTRACT_ADDRESS src/MyContract.sol:MyContract \
  --chain base \
  --etherscan-api-key $BASESCAN_API_KEY \
  --constructor-args $(cast abi-encode "constructor(address,uint256)" $TOKEN_ADDR 1000)

# Verify on Blockscout (no API key needed)
forge verify-contract $CONTRACT_ADDRESS src/MyContract.sol:MyContract \
  --chain base \
  --verifier blockscout \
  --verifier-url https://base.blockscout.com/api/

OnchainKit

OnchainKit is Coinbase's React component library for building onchain apps on Base. It provides ready-made components for identity, transactions, swaps, wallets, and Farcaster frames.

Installation

npm install @coinbase/onchainkit
# Peer dependencies
npm install viem wagmi @tanstack/react-query

Provider Setup

import { OnchainKitProvider } from '@coinbase/onchainkit';
import { base } from 'viem/chains';

function App({ children }: { children: React.ReactNode }) {
  return (
    <OnchainKitProvider
      apiKey={process.env.NEXT_PUBLIC_CDP_API_KEY}
      chain={base}
    >
      {children}
    </OnchainKitProvider>
  );
}

Identity Components

Resolve onchain identity (ENS, Basenames, Farcaster):

import {
  Identity,
  Name,
  Avatar,
  Badge,
  Address,
} from '@coinbase/onchainkit/identity';

function UserProfile({ address }: { address: `0x${string}` }) {
  return (
    <Identity address={address} schemaId="0x...">
      <Avatar />
      <Name>
        <Badge />
      </Name>
      <Address />
    </Identity>
  );
}

Transaction Component

Submit transactions with built-in status tracking:

import { Transaction, TransactionButton, TransactionStatus } from '@coinbase/onchainkit/transaction';
import { baseSepolia } from 'viem/chains';

const contracts = [
  {
    address: '0xContractAddress' as `0x${string}`,
    abi: contractAbi,
    functionName: 'mint',
    args: [1n],
  },
];

function MintButton() {
  return (
    <Transaction
      chainId={baseSepolia.id}
      contracts={contracts}
      onStatus={(status) => console.log('tx status:', status)}
    >
      <TransactionButton text="Mint NFT" />
      <TransactionStatus />
    </Transaction>
  );
}

Swap Component

Token swaps powered by 0x/Uniswap:

import { Swap, SwapAmountInput, SwapButton, SwapToggle } from '@coinbase/onchainkit/swap';
import type { Token } from '@coinbase/onchainkit/token';

const ETH: Token = {
  name: 'Ethereum',
  address: '',
  symbol: 'ETH',
  decimals: 18,
  image: 'https://...',
  chainId: 8453,
};

const USDC: Token = {
  name: 'USD Coin',
  address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  symbol: 'USDC',
  decimals: 6,
  image: 'https://...',
  chainId: 8453,
};

function SwapWidget() {
  return (
    <Swap>
      <SwapAmountInput label="Sell" token={ETH} type="from" />
      <SwapToggle />
      <SwapAmountInput label="Buy" token={USDC} type="to" />
      <SwapButton />
    </Swap>
  );
}

Wallet Component

import {
  ConnectWallet,
  Wallet,
  WalletDropdown,
  WalletDropdownDisconnect,
} from '@coinbase/onchainkit/wallet';

function WalletConnect() {
  return (
    <Wallet>
      <ConnectWallet>
        <Avatar />
        <Name />
      </ConnectWallet>
      <WalletDropdown>
        <Identity />
        <WalletDropdownDisconnect />
      </WalletDropdown>
    </Wallet>
  );
}

Smart Wallet

Coinbase Smart Wallet is a smart contract wallet using passkeys (WebAuthn) for authentication. It follows ERC-4337 (account abstraction) so users do not need a seed phrase or browser extension.

How It Works

  1. User creates wallet via passkey (fingerprint, Face ID, or security key)
  2. A smart contract wallet is deployed on Base (counterfactual — deployed on first transaction)
  3. Transactions are signed with the passkey and submitted as UserOperations
  4. Compatible with ERC-4337 bundlers and paymasters

wagmi + Smart Wallet

import { http, createConfig } from 'wagmi';
import { base, baseSepolia } from 'wagmi/chains';
import { coinbaseWallet } from 'wagmi/connectors';

export const config = createConfig({
  chains: [base, baseSepolia],
  connectors: [
    coinbaseWallet({
      appName: 'My Base App',
      // preference: 'smartWalletOnly' forces smart wallet (no extension)
      preference: 'smartWalletOnly',
    }),
  ],
  transports: {
    [base.id]: http(),
    [baseSepolia.id]: http(),
  },
});

Sending Transactions from Smart Wallet

import { useWriteContract } from 'wagmi';

function MintWithSmartWallet() {
  const { writeContract } = useWriteContract();

  const handleMint = () => {
    writeContract({
      address: '0xContractAddress',
      abi: contractAbi,
      functionName: 'mint',
      args: [1n],
    });
  };

  return <button onClick={handleMint}>Mint</button>;
}

Batch Transactions (EIP-5792)

Smart Wallet supports wallet_sendCalls for atomic batching:

import { useWriteContracts } from 'wagmi/experimental';

function BatchMint() {
  const { writeContracts } = useWriteContracts();

  const handleBatch = () => {
    writeContracts({
      contracts: [
        {
          address: '0xTokenAddress',
          abi: erc20Abi,
          functionName: 'approve',
          args: ['0xSpender', 1000000n],
        },
        {
          address: '0xSpender',
          abi: spenderAbi,
          functionName: 'deposit',
          args: [1000000n],
        },
      ],
    });
  };

  return <button onClick={handleBatch}>Approve + Deposit</button>;
}

Paymaster (Gasless Transactions)

A paymaster sponsors gas fees so users pay zero gas. Coinbase Developer Platform provides a paymaster service for Base.

Setup

  1. Create a project at portal.cdp.coinbase.com
  2. Enable the Paymaster service
  3. Configure a paymaster policy (which contracts/methods to sponsor)
  4. Get your paymaster URL: https://api.developer.coinbase.com/rpc/v1/base/$CDP_API_KEY

wagmi + Paymaster (EIP-5792)

import { useWriteContracts } from 'wagmi/experimental';
import { base } from 'viem/chains';

const PAYMASTER_URL = `https://api.developer.coinbase.com/rpc/v1/base/${process.env.NEXT_PUBLIC_CDP_API_KEY}`;

function SponsoredMint() {
  const { writeContracts } = useWriteContracts();

  const handleMint = () => {
    writeContracts({
      contracts: [
        {
          address: '0xContractAddress',
          abi: contractAbi,
          functionName: 'mint',
          args: [1n],
        },
      ],
      capabilities: {
        paymasterService: {
          url: PAYMASTER_URL,
        },
      },
    });
  };

  return <button onClick={handleMint}>Mint (Gasless)</button>;
}

Viem + Paymaster (Direct UserOperation)

import { createPublicClient, http, encodeFunctionData } from 'viem';
import { base } from 'viem/chains';

const PAYMASTER_URL = `https://api.developer.coinbase.com/rpc/v1/base/${process.env.CDP_API_KEY}`;

async function sponsoredCall(
  smartWalletAddress: `0x${string}`,
  target: `0x${string}`,
  calldata: `0x${string}`
) {
  const client = createPublicClient({
    chain: base,
    transport: http(),
  });

  // pm_getPaymasterStubData returns gas estimates for the UserOp
  const paymasterResponse = await fetch(PAYMASTER_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'pm_getPaymasterStubData',
      params: [
        {
          sender: smartWalletAddress,
          callData: calldata,
          callGasLimit: '0x0',
          verificationGasLimit: '0x0',
          preVerificationGas: '0x0',
          maxFeePerGas: '0x0',
          maxPriorityFeePerGas: '0x0',
        },
        // EntryPoint v0.7
        '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
        `0x${base.id.toString(16)}`,
      ],
    }),
  });

  return paymasterResponse.json();
}

Paymaster Policy

Configure which transactions are sponsored in the Coinbase Developer Platform dashboard:

  • Contract allowlist — Only sponsor calls to specific contract addresses
  • Method allowlist — Only sponsor specific function selectors
  • Spend limits — Max gas per user per day/week/month
  • Rate limits — Max sponsored txs per user per time window

Bridging

Base uses the OP Stack standard bridge. ETH and ERC-20 tokens can be bridged between Ethereum L1 and Base L2.

Bridge ETH to Base (L1 -> L2)

import { createWalletClient, http, parseEther } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

// OptimismPortal on L1 (Ethereum mainnet)
const OPTIMISM_PORTAL = '0x49048044D57e1C92A77f79988d21Fa8fAF36f97B' as const;

const walletClient = createWalletClient({
  chain: mainnet,
  transport: http(),
  account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
});

// Deposit ETH to Base — sends to OptimismPortal with value
const hash = await walletClient.sendTransaction({
  to: OPTIMISM_PORTAL,
  value: parseEther('0.1'),
  // depositTransaction calldata for simple ETH transfer
  data: '0x',
});
// ETH arrives on Base in ~1-5 minutes

Bridge ETH from Base (L2 -> L1)

L2-to-L1 withdrawals follow the OP Stack withdrawal flow:

  1. Initiate withdrawal on L2 (sends message to L2ToL1MessagePasser)
  2. Wait for state root to be posted on L1 (~1 hour)
  3. Prove withdrawal on L1 (submit Merkle proof)
  4. Wait for challenge period (~7 days on mainnet)
  5. Finalize withdrawal on L1
import { createPublicClient, createWalletClient, http, parseEther } from 'viem';
import { base } from 'viem/chains';

// L2ToL1MessagePasser predeploy on Base
const L2_TO_L1_MESSAGE_PASSER = '0x4200000000000000000000000000000000000016' as const;

const l2WalletClient = createWalletClient({
  chain: base,
  transport: http(),
  account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
});

// Step 1: Initiate withdrawal on L2
const hash = await l2WalletClient.writeContract({
  address: L2_TO_L1_MESSAGE_PASSER,
  abi: [
    {
      name: 'initiateWithdrawal',
      type: 'function',
      stateMutability: 'payable',
      inputs: [
        { name: '_target', type: 'address' },
        { name: '_gasLimit', type: 'uint256' },
        { name: '_data', type: 'bytes' },
      ],
      outputs: [],
    },
  ],
  functionName: 'initiateWithdrawal',
  args: [
    l2WalletClient.account.address, // send to self on L1
    100_000n, // L1 gas limit for the relay
    '0x',
  ],
  value: parseEther('0.1'),
});
// Steps 2-4 require waiting and L1 transactions (use the Base Bridge UI or OP SDK)

Gas Model

Base follows the OP Stack gas model: L2 execution fee + L1 data fee.

L2 Execution Fee

Same as Ethereum: gasUsed * baseFee. Base uses EIP-1559 with a 2-second block time. Base fees are typically very low (0.001-0.01 gwei).

L1 Data Fee

Every L2 transaction is posted to Ethereum L1 as calldata or blobs. The L1 data fee covers this cost.

After EIP-4844 (Dencun upgrade, March 2024), L1 data is posted as blobs, reducing costs ~100x.

import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';

// GasPriceOracle predeploy
const GAS_PRICE_ORACLE = '0x420000000000000000000000000000000000000F' as const;

const gasPriceOracleAbi = [
  {
    name: 'getL1Fee',
    type: 'function',
    stateMutability: 'view',
    inputs: [{ name: '_data', type: 'bytes' }],
    outputs: [{ name: '', type: 'uint256' }],
  },
  {
    name: 'baseFeeScalar',
    type: 'function',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ name: '', type: 'uint32' }],
  },
  {
    name: 'blobBaseFeeScalar',
    type: 'function',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ name: '', type: 'uint32' }],
  },
] as const;

const client = createPublicClient({ chain: base, transport: http() });

// Estimate L1 data fee for a transaction
const l1Fee = await client.readContract({
  address: GAS_PRICE_ORACLE,
  abi: gasPriceOracleAbi,
  functionName: 'getL1Fee',
  args: ['0x...serializedTxData'],
});

Gas Estimation Tips

  • Total cost = L2 execution fee + L1 data fee
  • L1 data fee dominates for simple transactions
  • Minimize calldata size to reduce L1 costs (pack structs, use shorter data)
  • eth_estimateGas returns L2 gas only — add L1 data fee separately
  • viem's estimateTotalFee on OP Stack chains handles both components

Ecosystem

Major Base Protocols

ProtocolCategoryDescription
AerodromeDEXDominant DEX on Base (ve(3,3) model, Velodrome fork)
Uniswap V3DEXDeployed on Base with full feature set
Aave V3LendingLending/borrowing on Base
Compound IIILendingUSDC lending market
Extra FinanceYieldLeveraged yield farming
MoonwellLendingFork of Compound, native to Base
SeamlessLendingIntegrated lending protocol
BaseSwapDEXNative Base DEX
MorphoLendingPermissionless lending vaults

Key Token Addresses (Base Mainnet)

TokenAddress
WETH0x4200000000000000000000000000000000000006
USDC (native)0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
USDbC (bridged)0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6D
cbETH0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22
DAI0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb
AERO0x940181a94A35A4569E4529A3CDfB74e38FD98631

Last verified: 2025-04. Always verify onchain with cast code <address> --rpc-url https://mainnet.base.org before production use.

Key Differences from Other OP Stack Chains

AspectBaseOptimismOther OP Chains
Sequencer operatorCoinbaseOptimism FoundationVaries
Fee recipientCoinbase multisigOptimism FoundationVaries
Smart WalletNative Coinbase integrationNot defaultNot default
PaymasterCoinbase Developer PlatformThird-party onlyThird-party only
OnchainKitFull supportPartial (identity only)None
Superchain membershipYesYes (leader)Varies
Fault proofsStage 1 (planned)Stage 1Varies

Useful Commands

# Check L1 data fee for a tx
cast call 0x420000000000000000000000000000000000000F \
  "getL1Fee(bytes)(uint256)" 0x... \
  --rpc-url https://mainnet.base.org

# Read GasPriceOracle scalars
cast call 0x420000000000000000000000000000000000000F \
  "baseFeeScalar()(uint32)" \
  --rpc-url https://mainnet.base.org

# Check if address is a contract (smart wallet check)
cast code $ADDRESS --rpc-url https://mainnet.base.org

# Get current L2 base fee
cast base-fee --rpc-url https://mainnet.base.org

# Decode L1 batch data
cast call 0x4200000000000000000000000000000000000015 \
  "l1BlockNumber()(uint64)" \
  --rpc-url https://mainnet.base.org

References

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/maker/SKILL.md

MakerDAO / Sky Protocol

MakerDAO is the protocol behind DAI, the largest decentralized stablecoin on Ethereum. Users lock collateral in Maker Vaults (formerly CDPs) to mint DAI. The protocol charges a stability fee (interest) on outstanding DAI debt and maintains a target price of $1 through the DAI Savings Rate (DSR) and liquidation mechanisms. In 2024, MakerDAO rebranded to Sky Protocol, introducing USDS (upgraded DAI) and SKY (upgraded MKR). Both old and new tokens coexist -- DAI/MKR are not deprecated.

What You Probably Got Wrong

LLMs consistently hallucinate Maker contract interfaces. The system is complex: all accounting happens in the Vat using internal rad/wad/ray units. Users interact through DSProxy + DssProxyActions, NOT directly with the Vat. These corrections are non-negotiable.

  • Maker is mid-rebrand to Sky. DAI is becoming USDS. MKR is becoming SKY. Both coexist on mainnet. The old contracts still work. The new Sky contracts wrap/unwrap between old and new tokens. If someone says "Maker" they might mean either system. Always check which token set they need.

  • You do NOT call the Vat directly. Normal users interact through a DSProxy contract that delegates calls to DssProxyActions. The Vat uses internal accounting units (rad = 10^45) that require precise math. DssProxyActions handles this conversion. If you see raw Vat.frob() calls, you are writing low-level code that will almost certainly have precision errors.

  • DAI has two representations. Internal DAI in the Vat (vat.dai(address)) is measured in rad (10^45). External DAI (the ERC-20 token) is measured in wad (10^18). The DaiJoin adapter converts between them. Never confuse the two.

  • Stability fees accrue continuously. The Jug contract compounds the stability fee rate into an ever-increasing rate accumulator per collateral type (ilk). Debt is stored as normalized debt (art) in the Vat. Actual debt = art * rate. You MUST call jug.drip(ilk) to update the rate before calculating accurate debt.

  • Liquidation 2.0 uses Dutch auctions, not English auctions. The old Flipper (English auctions) was replaced by the Clipper (Dutch auctions) in Liquidation 2.0. Dutch auctions start at a high price and decrease over time. Bidders call clipper.take(), not bid().

  • Vault IDs (cdpId) are NOT the same as ilk identifiers. An ilk (e.g., ETH-A, WBTC-A) defines the collateral type and its risk parameters. A vault (CDP) is a specific user position within an ilk. The CdpManager maps vault IDs to (ilk, urn address) pairs.

  • DSR and USDS Savings Rate (sUSDS) are different contracts. The original DSR uses DsrManager/Pot. The new Sky system uses the sUSDS token (ERC-4626 vault). They are separate yield sources with potentially different rates.

Architecture Overview

User -> DSProxy -> DssProxyActions -> | CdpManager (vault management)
                                      | Vat (core accounting)
                                      | Jug (stability fees)
                                      | DaiJoin (DAI minting)
                                      | GemJoin (collateral locking)

User -> DsrManager -> Pot (DAI Savings Rate)

User -> DaiUsds (upgrade) -> USDS token
User -> MkrSky (upgrade) -> SKY token
User -> sUSDS vault -> USDS Savings Rate (ERC-4626)

Keepers -> Dog (liquidation trigger) -> Clipper (Dutch auction)

Unit System (CRITICAL)

Maker uses three fixed-point number types internally. Getting these wrong causes silent precision loss or reverts.

UnitDecimalsUsed ForExample
wad10^18Token amounts (DAI, collateral), normalized debt (art)1.5 DAI = 1500000000000000000
ray10^27Rate accumulators, per-second rates1.0 rate = 1000000000000000000000000000
rad10^45Internal DAI balance in Vat (vat.dai())wad * ray = rad

Arithmetic rules:

  • wad * wad / WAD = wad
  • wad * ray / RAY = wad (used for debt calculation: art * rate)
  • rad / ray = wad (converting internal DAI to external)
  • rad / wad = ray
const WAD = 10n ** 18n;
const RAY = 10n ** 27n;
const RAD = 10n ** 45n;

function wmul(x: bigint, y: bigint): bigint {
  return (x * y + WAD / 2n) / WAD;
}

function rmul(x: bigint, y: bigint): bigint {
  return (x * y + RAY / 2n) / RAY;
}

function rdiv(x: bigint, y: bigint): bigint {
  return (x * RAY + y / 2n) / y;
}

Core Contracts

Vat -- Core Accounting Engine

The Vat is the central ledger. All collateral positions and DAI balances are recorded here. It never touches external tokens directly.

Key state:

  • ilks[ilk].Art -- total normalized debt for this collateral type (wad)
  • ilks[ilk].rate -- accumulated stability fee rate (ray)
  • ilks[ilk].spot -- collateral price with safety margin (ray)
  • ilks[ilk].line -- debt ceiling for this ilk (rad)
  • ilks[ilk].dust -- minimum debt per vault (rad)
  • urns[ilk][urn].ink -- locked collateral (wad)
  • urns[ilk][urn].art -- normalized debt (wad)
  • dai[address] -- internal DAI balance (rad)

The core function frob(ilk, urn, dink, dart) modifies a vault's collateral (dink) and debt (dart). Positive values add, negative values remove.

CdpManager -- Vault Registry

Maps sequential vault IDs to (ilk, urn) pairs. Users create vaults through CdpManager.open(ilk, usr) which returns a cdpId. The manager owns the Vat urns and delegates control via CdpManager.cdpCan.

const cdpManagerAbi = [
  {
    name: "open",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "ilk", type: "bytes32" },
      { name: "usr", type: "address" },
    ],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "ilks",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "cdpId", type: "uint256" }],
    outputs: [{ name: "", type: "bytes32" }],
  },
  {
    name: "urns",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "cdpId", type: "uint256" }],
    outputs: [{ name: "", type: "address" }],
  },
  {
    name: "owns",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "cdpId", type: "uint256" }],
    outputs: [{ name: "", type: "address" }],
  },
  {
    name: "count",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "usr", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "first",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "usr", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "list",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "cdpId", type: "uint256" }],
    outputs: [
      { name: "prev", type: "uint256" },
      { name: "next", type: "uint256" },
    ],
  },
] as const;

DssProxyActions -- User-Facing API

DssProxyActions is a library contract called via delegatecall from a user's DSProxy. It bundles multi-step vault operations into single transactions.

Key functions (called through DSProxy):

  • open(cdpManager, ilk, dsProxy) -- create a new vault
  • lockETH(cdpManager, ethJoin, cdpId) -- deposit ETH collateral (payable)
  • lockGem(cdpManager, gemJoin, cdpId, wad) -- deposit ERC-20 collateral
  • draw(cdpManager, jug, daiJoin, cdpId, wad) -- generate DAI from vault
  • wipe(cdpManager, daiJoin, cdpId, wad) -- repay DAI debt
  • wipeAll(cdpManager, daiJoin, cdpId) -- repay all DAI debt
  • freeETH(cdpManager, ethJoin, cdpId, wad) -- withdraw ETH collateral
  • freeGem(cdpManager, gemJoin, cdpId, wad) -- withdraw ERC-20 collateral
  • lockETHAndDraw(cdpManager, jug, ethJoin, daiJoin, cdpId, wadDai) -- lock ETH + draw DAI in one tx
  • openLockETHAndDraw(cdpManager, jug, ethJoin, daiJoin, ilk, wadDai) -- open vault + lock ETH + draw DAI

Jug -- Stability Fee Accumulator

The Jug tracks per-ilk stability fee rates. Calling jug.drip(ilk) updates vat.ilks[ilk].rate by compounding the fee since the last update.

const jugAbi = [
  {
    name: "drip",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "ilk", type: "bytes32" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "ilks",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "ilk", type: "bytes32" }],
    outputs: [
      { name: "duty", type: "uint256" },
      { name: "rho", type: "uint256" },
    ],
  },
] as const;

duty is the per-second stability fee rate (ray). rho is the last drip timestamp.

Join Adapters

Join adapters move tokens between the external ERC-20 world and the internal Vat accounting.

  • GemJoin -- locks collateral tokens. One per collateral type. join(urn, wad) moves tokens into the Vat. exit(usr, wad) withdraws them.
  • DaiJoin -- converts between internal rad-denominated DAI and external ERC-20 DAI. join(urn, wad) burns ERC-20 DAI and credits internal DAI. exit(usr, wad) mints ERC-20 DAI from internal DAI.
  • ETHJoin -- special join adapter that wraps native ETH into the Vat (no ERC-20 needed).

Opening a Vault via DSProxy

The standard flow for opening a vault and generating DAI:

import {
  createPublicClient,
  createWalletClient,
  http,
  parseEther,
  encodeFunctionData,
  type Address,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);

const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const CDP_MANAGER = "0x5ef30b9986345249bc32d8928B7ee64DE9435E39" as const;
const MCD_JUG = "0x19c0976f590D67707E62397C87829d896Dc0f1F1" as const;
const MCD_JOIN_ETH_A = "0x2F0b23f53734252Bda2277357e97e1517d6B042A" as const;
const MCD_JOIN_DAI = "0x9759A6Ac90977b93B58547b4A71c78317f391A28" as const;
const PROXY_ACTIONS = "0x82ecD135Dce65Fbc6DbdD0e4237E0AF93FFD5038" as const;

// ETH-A ilk identifier (bytes32)
const ETH_A_ILK = "0x4554482d41000000000000000000000000000000000000000000000000000000" as const;

const proxyActionsAbi = [
  {
    name: "openLockETHAndDraw",
    type: "function",
    stateMutability: "payable",
    inputs: [
      { name: "manager", type: "address" },
      { name: "jug", type: "address" },
      { name: "ethJoin", type: "address" },
      { name: "daiJoin", type: "address" },
      { name: "ilk", type: "bytes32" },
      { name: "wadD", type: "uint256" },
    ],
    outputs: [{ name: "cdp", type: "uint256" }],
  },
  {
    name: "lockETHAndDraw",
    type: "function",
    stateMutability: "payable",
    inputs: [
      { name: "manager", type: "address" },
      { name: "jug", type: "address" },
      { name: "ethJoin", type: "address" },
      { name: "daiJoin", type: "address" },
      { name: "cdp", type: "uint256" },
      { name: "wadD", type: "uint256" },
    ],
    outputs: [],
  },
  {
    name: "wipeAllAndFreeETH",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "manager", type: "address" },
      { name: "ethJoin", type: "address" },
      { name: "daiJoin", type: "address" },
      { name: "cdp", type: "uint256" },
      { name: "wadC", type: "uint256" },
    ],
    outputs: [],
  },
] as const;

const dsProxyAbi = [
  {
    name: "execute",
    type: "function",
    stateMutability: "payable",
    inputs: [
      { name: "_target", type: "address" },
      { name: "_data", type: "bytes" },
    ],
    outputs: [{ name: "response", type: "bytes32" }],
  },
] as const;

Execute via DSProxy

All DssProxyActions calls go through your DSProxy's execute(target, data):

async function openVaultAndDrawDai(
  dsProxy: Address,
  ethAmount: bigint,
  daiAmount: bigint
): Promise<`0x${string}`> {
  const calldata = encodeFunctionData({
    abi: proxyActionsAbi,
    functionName: "openLockETHAndDraw",
    args: [CDP_MANAGER, MCD_JUG, MCD_JOIN_ETH_A, MCD_JOIN_DAI, ETH_A_ILK, daiAmount],
  });

  const { request } = await publicClient.simulateContract({
    address: dsProxy,
    abi: dsProxyAbi,
    functionName: "execute",
    args: [PROXY_ACTIONS, calldata],
    value: ethAmount,
    account: account.address,
  });

  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    throw new Error("openLockETHAndDraw reverted");
  }

  return hash;
}

Reading Vault State

const vatAbi = [
  {
    name: "ilks",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "ilk", type: "bytes32" }],
    outputs: [
      { name: "Art", type: "uint256" },
      { name: "rate", type: "uint256" },
      { name: "spot", type: "uint256" },
      { name: "line", type: "uint256" },
      { name: "dust", type: "uint256" },
    ],
  },
  {
    name: "urns",
    type: "function",
    stateMutability: "view",
    inputs: [
      { name: "ilk", type: "bytes32" },
      { name: "urn", type: "address" },
    ],
    outputs: [
      { name: "ink", type: "uint256" },
      { name: "art", type: "uint256" },
    ],
  },
  {
    name: "dai",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "usr", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
] as const;

const MCD_VAT = "0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B" as const;

async function getVaultInfo(cdpId: bigint) {
  const [ilk, urn] = await Promise.all([
    publicClient.readContract({
      address: CDP_MANAGER,
      abi: cdpManagerAbi,
      functionName: "ilks",
      args: [cdpId],
    }),
    publicClient.readContract({
      address: CDP_MANAGER,
      abi: cdpManagerAbi,
      functionName: "urns",
      args: [cdpId],
    }),
  ]);

  const [ilkData, urnData] = await Promise.all([
    publicClient.readContract({
      address: MCD_VAT,
      abi: vatAbi,
      functionName: "ilks",
      args: [ilk],
    }),
    publicClient.readContract({
      address: MCD_VAT,
      abi: vatAbi,
      functionName: "urns",
      args: [ilk, urn],
    }),
  ]);

  const ink = urnData[0]; // locked collateral (wad)
  const art = urnData[1]; // normalized debt (wad)
  const rate = ilkData[1]; // accumulated rate (ray)
  const spot = ilkData[2]; // price with safety margin (ray)

  // Actual debt = art * rate (result in rad, divide by RAY for wad)
  const RAY = 10n ** 27n;
  const debt = (art * rate + RAY - 1n) / RAY; // round up
  // Collateral value = ink * spot (result in rad, divide by RAY for wad)
  const collateralValue = (ink * spot) / RAY;

  return { ilk, urn, ink, art, rate, spot, debt, collateralValue };
}

DAI Savings Rate (DSR)

The DSR lets DAI holders earn yield by depositing into the Pot contract. DsrManager simplifies the Pot interaction.

const DSR_MANAGER = "0x373238337Bfe1146fb49989fc222523f83081dDb" as const;
const MCD_POT = "0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7" as const;

const dsrManagerAbi = [
  {
    name: "join",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "dst", type: "address" },
      { name: "wad", type: "uint256" },
    ],
    outputs: [],
  },
  {
    name: "exit",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "dst", type: "address" },
      { name: "wad", type: "uint256" },
    ],
    outputs: [],
  },
  {
    name: "exitAll",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "dst", type: "address" }],
    outputs: [],
  },
  {
    name: "pieOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "usr", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
] as const;

const potAbi = [
  {
    name: "chi",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "dsr",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "rho",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "drip",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
] as const;

async function getDsrInfo() {
  const [dsr, chi, rho] = await Promise.all([
    publicClient.readContract({
      address: MCD_POT,
      abi: potAbi,
      functionName: "dsr",
    }),
    publicClient.readContract({
      address: MCD_POT,
      abi: potAbi,
      functionName: "chi",
    }),
    publicClient.readContract({
      address: MCD_POT,
      abi: potAbi,
      functionName: "rho",
    }),
  ]);

  // DSR APY = dsr^(seconds_per_year) - 1
  // dsr is a per-second rate in ray (10^27)
  const RAY = 10n ** 27n;
  const dsrFloat = Number(dsr) / Number(RAY);
  const dsrApy = (Math.pow(dsrFloat, 31536000) - 1) * 100;

  return { dsr, chi, rho, dsrApy };
}

async function depositToDsr(daiAmount: bigint): Promise<`0x${string}`> {
  const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" as const;

  // Approve DsrManager to spend DAI
  const approveHash = await walletClient.writeContract({
    address: DAI,
    abi: [
      {
        name: "approve",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [
          { name: "spender", type: "address" },
          { name: "amount", type: "uint256" },
        ],
        outputs: [{ name: "", type: "bool" }],
      },
    ] as const,
    functionName: "approve",
    args: [DSR_MANAGER, daiAmount],
  });
  const approveReceipt = await publicClient.waitForTransactionReceipt({
    hash: approveHash,
  });
  if (approveReceipt.status !== "success") {
    throw new Error("DAI approval for DSR failed");
  }

  // Deposit DAI into DSR
  const { request } = await publicClient.simulateContract({
    address: DSR_MANAGER,
    abi: dsrManagerAbi,
    functionName: "join",
    args: [account.address, daiAmount],
    account: account.address,
  });

  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    throw new Error("DSR deposit reverted");
  }

  return hash;
}

async function getDsrBalance(user: Address): Promise<bigint> {
  const RAY = 10n ** 27n;

  const [pie, chi] = await Promise.all([
    publicClient.readContract({
      address: DSR_MANAGER,
      abi: dsrManagerAbi,
      functionName: "pieOf",
      args: [user],
    }),
    publicClient.readContract({
      address: MCD_POT,
      abi: potAbi,
      functionName: "chi",
    }),
  ]);

  // DAI balance = pie * chi / RAY
  return (pie * chi) / RAY;
}

Liquidation 2.0 (Dutch Auctions)

When a vault's collateral ratio drops below the liquidation ratio, keepers trigger liquidation via the Dog contract. The Dog starts a Dutch auction via the Clipper for that ilk.

Liquidation Flow

  1. Dog.bark(ilk, urn, keeper) -- triggers liquidation, creates a Clipper auction
  2. Clipper starts at a high price (using calc -- an AbacI price calculator)
  3. Price decreases over time according to the price curve
  4. Anyone calls Clipper.take(id, amt, max, who, data) to buy collateral
  5. Remaining collateral (if any) returns to the vault owner
const MCD_DOG = "0x135954d155898D42C90D2a57824C690e0c7BEf1B" as const;

const dogAbi = [
  {
    name: "bark",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "ilk", type: "bytes32" },
      { name: "urn", type: "address" },
      { name: "kpr", type: "address" },
    ],
    outputs: [{ name: "id", type: "uint256" }],
  },
  {
    name: "ilks",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "ilk", type: "bytes32" }],
    outputs: [
      { name: "clip", type: "address" },
      { name: "chop", type: "uint256" },
      { name: "hole", type: "uint256" },
      { name: "dirt", type: "uint256" },
    ],
  },
] as const;

const clipperAbi = [
  {
    name: "take",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "id", type: "uint256" },
      { name: "amt", type: "uint256" },
      { name: "max", type: "uint256" },
      { name: "who", type: "address" },
      { name: "data", type: "bytes" },
    ],
    outputs: [],
  },
  {
    name: "sales",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "id", type: "uint256" }],
    outputs: [
      { name: "pos", type: "uint256" },
      { name: "tab", type: "uint256" },
      { name: "lot", type: "uint256" },
      { name: "usr", type: "address" },
      { name: "tic", type: "uint96" },
      { name: "top", type: "uint256" },
    ],
  },
  {
    name: "getStatus",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "id", type: "uint256" }],
    outputs: [
      { name: "needsRedo", type: "bool" },
      { name: "price", type: "uint256" },
      { name: "lot", type: "uint256" },
      { name: "tab", type: "uint256" },
    ],
  },
  {
    name: "count",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "list",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256[]" }],
  },
] as const;

Participating in a Dutch Auction

async function takeFromAuction(
  clipperAddress: Address,
  auctionId: bigint,
  collateralAmount: bigint,
  maxPrice: bigint
): Promise<`0x${string}`> {
  // Check auction status
  const status = await publicClient.readContract({
    address: clipperAddress,
    abi: clipperAbi,
    functionName: "getStatus",
    args: [auctionId],
  });

  const [needsRedo, currentPrice, lot, tab] = status;

  if (needsRedo) {
    throw new Error("Auction needs redo -- price has gone stale");
  }

  if (currentPrice > maxPrice) {
    throw new Error(
      `Current price ${currentPrice} exceeds max ${maxPrice}. Wait for price to decrease.`
    );
  }

  if (lot === 0n || tab === 0n) {
    throw new Error("Auction is complete -- no collateral remaining");
  }

  // take() requires DAI approval to the Vat (internal DAI)
  // Keepers typically pre-approve the Clipper in the Vat
  const { request } = await publicClient.simulateContract({
    address: clipperAddress,
    abi: clipperAbi,
    functionName: "take",
    args: [auctionId, collateralAmount, maxPrice, account.address, "0x"],
    account: account.address,
  });

  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    throw new Error("Auction take reverted");
  }

  return hash;
}

MKR Governance

MKR holders vote on protocol parameters through the Chief contract. The voting flow uses a delegate + hat pattern.

Executive Voting

Executive votes change live protocol parameters. They are spell contracts that are cast when enough MKR is staked on them.

const MCD_GOV = "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2" as const; // MKR token
const MCD_ADM = "0x0a3f6849f78076aefaDf113F5BED87720274dDC0" as const; // DSChief

const chiefAbi = [
  {
    name: "vote",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "yays", type: "address[]" }],
    outputs: [{ name: "", type: "bytes32" }],
  },
  {
    name: "lock",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "wad", type: "uint256" }],
    outputs: [],
  },
  {
    name: "free",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "wad", type: "uint256" }],
    outputs: [],
  },
  {
    name: "hat",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "address" }],
  },
  {
    name: "approvals",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "candidate", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "deposits",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "usr", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
] as const;

Governance Flow

  1. Lock MKR in Chief: chief.lock(amount)
  2. Vote for a spell: chief.vote([spellAddress])
  3. If the spell gets the most MKR, it becomes the hat
  4. Anyone can lift the hat to make it the active authority
  5. The spell is cast to execute parameter changes

Sky Protocol Rebranding

MakerDAO rebranded to Sky Protocol in 2024. New tokens:

  • USDS -- upgraded DAI (1:1 convertible)
  • SKY -- upgraded MKR (1 MKR = 24,000 SKY)
  • sUSDS -- USDS savings token (ERC-4626), replaces DSR for USDS holders

Token Migration

const DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" as const;
const USDS = "0xdC035D45d973E3EC169d2276DDab16f1e407384F" as const;
const MKR = "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2" as const;
const SKY = "0x56072C95FAA7932F4D8Aa042BE0611d2a2CE73a5" as const;
const DAI_USDS = "0x3225737a9Bbb6473CB4a45b7244ACa2BeFdB276A" as const; // DaiUsds converter
const MKR_SKY = "0xBDcFCA946b6CDd965f99a839e4435Bcdc1bc470B" as const; // MkrSky converter
const SUSDS = "0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD" as const; // sUSDS vault

const daiUsdsAbi = [
  {
    name: "daiToUsds",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "usr", type: "address" },
      { name: "wad", type: "uint256" },
    ],
    outputs: [],
  },
  {
    name: "usdsToDai",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "usr", type: "address" },
      { name: "wad", type: "uint256" },
    ],
    outputs: [],
  },
] as const;

const mkrSkyAbi = [
  {
    name: "mkrToSky",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "usr", type: "address" },
      { name: "mkrAmt", type: "uint256" },
    ],
    outputs: [],
  },
  {
    name: "skyToMkr",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "usr", type: "address" },
      { name: "skyAmt", type: "uint256" },
    ],
    outputs: [],
  },
  {
    name: "rate",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
] as const;

// sUSDS is an ERC-4626 vault for USDS savings
const susdsAbi = [
  {
    name: "deposit",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "assets", type: "uint256" },
      { name: "receiver", type: "address" },
    ],
    outputs: [{ name: "shares", type: "uint256" }],
  },
  {
    name: "withdraw",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "assets", type: "uint256" },
      { name: "receiver", type: "address" },
      { name: "owner", type: "address" },
    ],
    outputs: [{ name: "shares", type: "uint256" }],
  },
  {
    name: "redeem",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "shares", type: "uint256" },
      { name: "receiver", type: "address" },
      { name: "owner", type: "address" },
    ],
    outputs: [{ name: "assets", type: "uint256" }],
  },
  {
    name: "convertToAssets",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "shares", type: "uint256" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "convertToShares",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "assets", type: "uint256" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "totalAssets",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
] as const;

Upgrading DAI to USDS

async function upgradeDaiToUsds(amount: bigint): Promise<`0x${string}`> {
  // Approve DaiUsds converter to spend DAI
  const approveHash = await walletClient.writeContract({
    address: DAI,
    abi: [
      {
        name: "approve",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [
          { name: "spender", type: "address" },
          { name: "amount", type: "uint256" },
        ],
        outputs: [{ name: "", type: "bool" }],
      },
    ] as const,
    functionName: "approve",
    args: [DAI_USDS, amount],
  });
  const approveReceipt = await publicClient.waitForTransactionReceipt({
    hash: approveHash,
  });
  if (approveReceipt.status !== "success") {
    throw new Error("DAI approval for upgrade failed");
  }

  // Convert DAI -> USDS (1:1)
  const { request } = await publicClient.simulateContract({
    address: DAI_USDS,
    abi: daiUsdsAbi,
    functionName: "daiToUsds",
    args: [account.address, amount],
    account: account.address,
  });

  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    throw new Error("DAI to USDS conversion reverted");
  }

  return hash;
}

USDS Savings Rate (sUSDS)

async function depositToSusds(usdsAmount: bigint): Promise<{
  hash: `0x${string}`;
  shares: bigint;
}> {
  // Approve sUSDS vault to spend USDS
  const approveHash = await walletClient.writeContract({
    address: USDS,
    abi: [
      {
        name: "approve",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [
          { name: "spender", type: "address" },
          { name: "amount", type: "uint256" },
        ],
        outputs: [{ name: "", type: "bool" }],
      },
    ] as const,
    functionName: "approve",
    args: [SUSDS, usdsAmount],
  });
  const approveReceipt = await publicClient.waitForTransactionReceipt({
    hash: approveHash,
  });
  if (approveReceipt.status !== "success") {
    throw new Error("USDS approval for sUSDS failed");
  }

  // Deposit USDS into sUSDS vault
  const { request, result } = await publicClient.simulateContract({
    address: SUSDS,
    abi: susdsAbi,
    functionName: "deposit",
    args: [usdsAmount, account.address],
    account: account.address,
  });

  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    throw new Error("sUSDS deposit reverted");
  }

  return { hash, shares: result };
}

Spark Protocol

Spark Protocol is an Aave V3 fork maintained by the Maker/Sky ecosystem. It uses DAI/USDS as its primary lending asset with preferential rates backed by the Maker D3M (Direct Deposit Module).

Key difference from vanilla Aave V3: Spark has a direct credit line from Maker, so DAI/USDS liquidity is deep and rates are governance-controlled.

Spark uses standard Aave V3 interfaces -- see the Aave skill for integration patterns. The Pool contract address for Spark on Ethereum mainnet is 0xC13e21B648A5Ee794902342038FF3aDAB66BE987.

DSProxy Setup

Most users need a DSProxy before interacting with Maker Vaults. The ProxyRegistry creates one per address.

const PROXY_REGISTRY = "0x4678f0a6958e4D2Bc4F1BAF7Bc52E8F3564f3fE4" as const;

const proxyRegistryAbi = [
  {
    name: "build",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [],
    outputs: [{ name: "proxy", type: "address" }],
  },
  {
    name: "proxies",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "owner", type: "address" }],
    outputs: [{ name: "", type: "address" }],
  },
] as const;

async function getOrCreateProxy(): Promise<Address> {
  const existing = await publicClient.readContract({
    address: PROXY_REGISTRY,
    abi: proxyRegistryAbi,
    functionName: "proxies",
    args: [account.address],
  });

  const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as Address;
  if (existing !== ZERO_ADDRESS) {
    return existing;
  }

  const { request } = await publicClient.simulateContract({
    address: PROXY_REGISTRY,
    abi: proxyRegistryAbi,
    functionName: "build",
    account: account.address,
  });

  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") {
    throw new Error("DSProxy creation reverted");
  }

  const proxyAddress = await publicClient.readContract({
    address: PROXY_REGISTRY,
    abi: proxyRegistryAbi,
    functionName: "proxies",
    args: [account.address],
  });

  return proxyAddress;
}

Common Patterns

Calculate Vault Collateralization Ratio

async function getCollateralizationRatio(cdpId: bigint): Promise<{
  ratio: number;
  isUnsafe: boolean;
}> {
  const vault = await getVaultInfo(cdpId);
  const RAY = 10n ** 27n;

  if (vault.art === 0n) {
    return { ratio: Infinity, isUnsafe: false };
  }

  // debt = art * rate (in rad), collateralValue = ink * spot (in rad)
  const debt = vault.art * vault.rate;
  const collateralValue = vault.ink * vault.spot;

  if (debt === 0n) {
    return { ratio: Infinity, isUnsafe: false };
  }

  const ratio = Number(collateralValue * 10000n / debt) / 100;
  const isUnsafe = collateralValue < debt;

  return { ratio, isUnsafe };
}

Encode Ilk Name

import { toHex, padHex } from "viem";

function encodeIlk(name: string): `0x${string}` {
  return padHex(toHex(name), { size: 32, dir: "right" });
}

// encodeIlk("ETH-A") = 0x4554482d41000000000000000000000000000000000000000000000000000000

List User's Vaults

async function listUserVaults(user: Address): Promise<bigint[]> {
  const count = await publicClient.readContract({
    address: CDP_MANAGER,
    abi: cdpManagerAbi,
    functionName: "count",
    args: [user],
  });

  if (count === 0n) return [];

  const first = await publicClient.readContract({
    address: CDP_MANAGER,
    abi: cdpManagerAbi,
    functionName: "first",
    args: [user],
  });

  const vaults: bigint[] = [first];
  let current = first;

  for (let i = 1n; i < count; i++) {
    const [, next] = await publicClient.readContract({
      address: CDP_MANAGER,
      abi: cdpManagerAbi,
      functionName: "list",
      args: [current],
    });
    if (next === 0n) break;
    vaults.push(next);
    current = next;
  }

  return vaults;
}

Security Considerations

  • Dust limit: Every vault must maintain at least dust amount of debt (or zero debt). Partial repayments that leave debt below dust will revert.
  • Oracle delay: Maker uses OSM (Oracle Security Module) which delays price updates by 1 hour. This means liquidations use prices that are up to 1 hour old.
  • Liquidation penalty: The chop parameter (typically 13%) is added on top of the debt during liquidation. Maintain safe collateralization ratios.
  • DSProxy ownership: Your DSProxy is a smart contract wallet. If you lose access, you lose control of all vaults owned by that proxy.
  • Governance attacks: The Chief contract is vulnerable to flash loan governance attacks. The GSM (Governance Security Module) imposes a 48-hour delay on spell execution.

References

AI Skill Finder

Ask me what skills you need

What are you building?

Tell me what you're working on and I'll find the best agent skills for you.