DEV Community

Cover image for ๐Ÿช™ Day 29 of #30DaysOfSolidity โ€” Building a Collateral-Backed Stablecoin in Solidity โ€” Step-by-Step Guide
Saurav Kumar
Saurav Kumar

Posted on

๐Ÿช™ Day 29 of #30DaysOfSolidity โ€” Building a Collateral-Backed Stablecoin in Solidity โ€” Step-by-Step Guide

Author: Saurav Kumar
Tags: solidity, defi, stablecoin, ethereum, blockchain
Difficulty: Intermediate โ†’ Advanced
Reading Time: ~10 minutes

๐Ÿ’ก Introduction

In decentralized finance (DeFi), stablecoins play a crucial role in bridging traditional finance and crypto.
They provide price stability, liquidity, and on-chain utilityโ€”acting as the backbone of protocols like MakerDAO (DAI), Aave, and Curve.

In this guide, weโ€™ll build a collateral-backed stablecoin named StableUSD (sUSD) using Solidity and Foundry, demonstrating how to maintain a 1:1 peg to the US dollar through collateralization and oracle-based pricing.

This article is part of my DeFi Engineering Series, where I recreate real-world protocols from scratch.


๐Ÿง  What Youโ€™ll Learn

  • How stablecoins maintain their price peg
  • How to design collateralized minting and redemption flows
  • How to integrate oracles (mock or Chainlink-style)
  • How to implement collateralization ratios and burn/mint mechanics
  • How to test everything using Foundry

๐Ÿ“‚ Project File Structure

Hereโ€™s the Foundry project layout:

day-29-stablecoin/
โ”œโ”€ foundry.toml
โ”œโ”€ .gitignore
โ”œโ”€ script/
โ”‚   โ””โ”€ Deploy.s.sol
โ”œโ”€ src/
โ”‚   โ”œโ”€ StableUSD.sol
โ”‚   โ”œโ”€ OracleManager.sol
โ”‚   โ”œโ”€ CollateralPool.sol
โ”‚   โ”œโ”€ Treasury.sol
โ”‚   โ”œโ”€ MockOracle.sol
โ”‚   โ”œโ”€ MockERC20.sol
โ”‚   โ””โ”€ interfaces/
โ”‚       โ””โ”€ IAggregatorV3.sol
โ””โ”€ test/
    โ””โ”€ Stablecoin.t.sol
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฉ System Overview

Component Description
StableUSD.sol ERC20 token for sUSD, mint/burn controlled
OracleManager.sol Connects to Chainlink or mock price feeds
CollateralPool.sol Core logic for deposits, minting & redemption
Treasury.sol Admin contract for reserves and fee collection

๐Ÿ—๏ธ Architecture Diagram

User โ†’ CollateralPool โ†’ StableUSD  
           โ†“  
        OracleManager  
           โ†“  
         Treasury
Enter fullscreen mode Exit fullscreen mode
  • Users deposit collateral (e.g., WETH).
  • OracleManager provides price data in USD.
  • CollateralPool mints sUSD tokens based on collateral value.
  • Treasury manages reserves and stability actions.

โš™๏ธ Setup Commands

forge init day-29-stablecoin
cd day-29-stablecoin
forge install OpenZeppelin/openzeppelin-contracts
forge install foundry-rs/forge-std
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฉ Smart Contracts

๐Ÿ“ src/interfaces/IAggregatorV3.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IAggregatorV3 {
    function latestRoundData()
        external
        view
        returns (
            uint80,
            int256 answer,
            uint256,
            uint256,
            uint80
        );
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ src/StableUSD.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract StableUSD is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() ERC20("Stable USD", "sUSD") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function burn(address from, uint256 amount) external onlyRole(MINTER_ROLE) {
        _burn(from, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

โœ… Implements ERC20
โœ… Controlled mint/burn via roles


๐Ÿ“ src/OracleManager.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./interfaces/IAggregatorV3.sol";

contract OracleManager {
    IAggregatorV3 public oracle;

    constructor(address _oracle) {
        oracle = IAggregatorV3(_oracle);
    }

    function getLatestPrice() external view returns (uint256) {
        (, int256 answer,,,) = oracle.latestRoundData();
        require(answer > 0, "Invalid price");
        return uint256(answer);
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก Use Chainlink price feeds for live ETH/USD in production.


๐Ÿ“ src/MockOracle.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MockOracle {
    int256 private price;

    constructor(int256 _price) {
        price = _price;
    }

    function latestRoundData()
        external
        view
        returns (uint80, int256, uint256, uint256, uint80)
    {
        return (0, price, 0, 0, 0);
    }

    function updatePrice(int256 _price) external {
        price = _price;
    }
}
Enter fullscreen mode Exit fullscreen mode

โœ… Simulates Chainlink oracles for local testing


๐Ÿ“ src/MockERC20.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockERC20 is ERC20 {
    constructor(string memory name, string memory symbol) ERC20(name, symbol) {}

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

โœ… Mock collateral asset (like WETH) for testing mint/redeem logic


๐Ÿ“ src/CollateralPool.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./StableUSD.sol";
import "./OracleManager.sol";

contract CollateralPool {
    IERC20 public collateral;
    StableUSD public stable;
    OracleManager public oracle;

    uint256 public constant COLLATERAL_RATIO = 150; // 150%
    mapping(address => uint256) public collateralBalance;

    constructor(IERC20 _collateral, StableUSD _stable, OracleManager _oracle) {
        collateral = _collateral;
        stable = _stable;
        oracle = _oracle;
    }

    function deposit(uint256 amount) external {
        require(amount > 0, "Invalid amount");
        collateral.transferFrom(msg.sender, address(this), amount);
        collateralBalance[msg.sender] += amount;
    }

    function mint(uint256 amountCollateral) external {
        require(collateralBalance[msg.sender] >= amountCollateral, "Insufficient collateral");

        uint256 price = oracle.getLatestPrice();
        uint256 usdValue = (amountCollateral * price) / 1e8;
        uint256 mintAmount = (usdValue * 100) / COLLATERAL_RATIO;

        stable.mint(msg.sender, mintAmount * 1e18);
    }

    function redeem(uint256 sUSDAmount) external {
        stable.burn(msg.sender, sUSDAmount);

        uint256 price = oracle.getLatestPrice();
        uint256 collateralToReturn = (sUSDAmount * 1e8 * COLLATERAL_RATIO) / (price * 100);

        require(collateralBalance[msg.sender] >= collateralToReturn, "Not enough collateral");
        collateralBalance[msg.sender] -= collateralToReturn;
        collateral.transfer(msg.sender, collateralToReturn);
    }
}
Enter fullscreen mode Exit fullscreen mode

โšก Implements over-collateralized mint/redeem flow
โšก Maintains peg using oracle price
โšก 150% ratio ensures safety


๐Ÿ“ src/Treasury.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Treasury {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function withdraw(address token, address to, uint256 amount) external {
        require(msg.sender == owner, "Not authorized");
        (bool ok,) = token.call(abi.encodeWithSignature("transfer(address,uint256)", to, amount));
        require(ok, "Transfer failed");
    }
}
Enter fullscreen mode Exit fullscreen mode

โœ… Simple treasury for system fund management


๐Ÿ“ script/Deploy.s.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/StableUSD.sol";
import "../src/OracleManager.sol";
import "../src/CollateralPool.sol";
import "../src/MockERC20.sol";
import "../src/MockOracle.sol";

contract Deploy is Script {
    function run() external {
        vm.startBroadcast();

        MockERC20 collateral = new MockERC20("Wrapped ETH", "WETH");
        MockOracle oracle = new MockOracle(1800e8);
        StableUSD stable = new StableUSD();
        OracleManager oracleManager = new OracleManager(address(oracle));
        CollateralPool pool = new CollateralPool(collateral, stable, oracleManager);

        stable.grantRole(stable.MINTER_ROLE(), address(pool));

        vm.stopBroadcast();
    }
}
Enter fullscreen mode Exit fullscreen mode

โœ… Deploys all components in one transaction


๐Ÿ“ test/Stablecoin.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/StableUSD.sol";
import "../src/CollateralPool.sol";
import "../src/OracleManager.sol";
import "../src/MockERC20.sol";
import "../src/MockOracle.sol";

contract StablecoinTest is Test {
    StableUSD stable;
    OracleManager oracle;
    CollateralPool pool;
    MockERC20 collateral;

    function setUp() public {
        collateral = new MockERC20("Wrapped ETH", "WETH");
        stable = new StableUSD();
        oracle = new OracleManager(address(new MockOracle(1800e8)));
        pool = new CollateralPool(IERC20(address(collateral)), stable, oracle);
        stable.grantRole(stable.MINTER_ROLE(), address(pool));
    }

    function testMintStablecoin() public {
        collateral.mint(address(this), 1 ether);
        collateral.approve(address(pool), 1 ether);
        pool.deposit(1 ether);
        pool.mint(1 ether);
        assertGt(stable.balanceOf(address(this)), 0);
    }
}
Enter fullscreen mode Exit fullscreen mode

โœ… Tests deposit โ†’ mint flow
โœ… Confirms minting works using oracle-based pricing


๐Ÿงช Test & Run

forge build
forge test -vv
Enter fullscreen mode Exit fullscreen mode

๐Ÿงฎ Peg Example

If 1 ETH = $1800 and collateral ratio = 150%,
then 1 ETH mints:

(1800 * 100 / 150) = 1200 sUSD
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  Key Learnings

โœ… How stablecoins maintain a peg
โœ… Using oracles to stabilize token value
โœ… Over-collateralization mechanics
โœ… Writing and testing DeFi smart contracts


๐Ÿ›ก๏ธ Security & Design Notes

  • Always use verified Chainlink oracles in production.
  • Implement liquidation logic for under-collateralized users.
  • Add pause and governance controls for protocol safety.
  • Track debt positions per user in a full system.

๐Ÿš€ Next Steps

  • Multi-collateral vaults (ETH, USDC, WBTC)
  • Dynamic interest and stability fees
  • DAO-based parameter governance
  • Integration with Uniswap for peg stabilization

๐Ÿงญ Conclusion

Stablecoins are the core monetary layer of DeFi โ€” building one from scratch deepens your understanding of price oracles, collateralization, and supply control.

This StableUSD demo shows how trustless monetary systems can maintain stability purely through code and math.

Top comments (0)