TIL

블록체인: Testnet 토큰 얻기

https://velog.io/@minkwan/%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-Smart-Contract이전에 작성한 글에서, Smart Contract 로직을 작성하고 Monad Testnet에 배포하는 법을 다루었습니다.

2025년 11월 27일5min read

1. 문제 상황 🎯

https://velog.io/@minkwan/%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-Smart-Contract

이전에 작성한 글에서, Smart Contract 로직을 작성하고 Monad Testnet에 배포하는 법을 다루었습니다. 글에서는 언급하지 않았지만 실패했습니다.

Monad Testnet은 Monad의 기능을 미리 경험할 수 있도록 제공되는 Public Test Network입니다. Testnet은 안전하고 통제된 환경에서 새로운 블록체인 기능, Smart Contract, 그리고 DApp을 실제 금전적 위험 없이 철저히 테스트하고 검증하기 위해 사용합니다.

Testnet 토큰은 실제 가치가 없고 오직 테스트 목적으로만 사용되는, Mainnet 토큰과 유사하게 작동하는 가상의 자산입니다.

제가 수강하는 강의에서는 Monad Faucet이라는 서비스에서 지갑 주소를 입력한 뒤 Monad Testnet 토큰을 수령받으면 된다고 설명했습니다. 그런데 Monad Faucet의 정책이 그 사이에 달라졌는지는 알 수 없으나, Your address holds less than 0.03 ETH on Ethereum.이라는 메시지가 나왔습니다.

천천히 생각해 보죠. 어차피 Testnet 토큰인데 왜 이런 최소 잔액 요구 사항이 생겨났을까요?

Testnet 토큰은 금전적 가치가 없더라도, 악용자들이 봇을 사용하여 토큰을 대량으로 확보한 뒤 네트워크에 불필요한 트랜잭션을 집중적으로 발생시켜 DDoS를 유발함으로써 정당한 개발자들의 테스트 환경을 교란하고 네트워크의 안정성을 해칠 수 있습니다.

처음부터 테스트 목적으로 일정 금액을 예치하는 것은 너무 불편하고 까다로운 것이 아닌가 하는 생각이 들었습니다. 그래서 조금 더 쉬운 방법을 찾아보게 되었습니다.

Monad Testnet 토큰 얻기 파트는 별도의 글로 분리할 가치가 있다고 생각했습니다. 앞으로 블록체인을 공부하며 Testnet에 배포하는 일이 비일비재할 것이기 때문이죠.

이제 Faucet Trade를 소개하겠습니다. 향후 Monad Faucet과 같은 서비스에 일정 금액을 예치한 후 Testnet 토큰을 받을 생각입니다. Faucet Trade도 최소 잔액 요구 사항이 발생할 수 있고, 특정 서비스에 종속되는 것이 그리 바람직하진 않으니까요.


2. Faucet Trade 🎯

Faucet Trade는 여러 Testnet에서 개발자와 테스터가 무료로 테스트 토큰을 즉시 받을 수 있게 해주는 웹 기반 Testnet faucet 서비스입니다. 웹사이트에서 사용자는 사용할 네트워크(Monad Testnet의 MON 등)를 선택하고 지갑 주소를 입력하면 그 네트워크의 테스트 토큰을 요청해 받을 수 있어, Smart Contract 배포, 트랜잭션 실험 등 실제 Mainnet 자금을 쓰지 않고도 기능을 확인할 수 있게 해줍니다.

Faucet Trade에서 토큰을 받는 절차는 매우 간단합니다.

지갑 주소 입력 => @faucet_trade 공식 x(구 트위터) 계정 팔로우 및 트윗 작성 => 트윗 URL 제출 => CAPTCHA 인증 순서로 진행됩니다.

계정을 팔로우하고 트윗을 작성하라는 절차를 통해, 진입 장벽이 높은 일반적인 faucet 서비스들과 달리 UX를 중시한다는 점을 알 수 있고, 동시에 무료 마케팅 효과까지 취한다는 점을 파악할 수 있습니다. 현명한 전략이네요.

2-1. 지갑 주소 입력 ✅

Testnet 토큰을 받을 지갑 주소를 입력하니 tweet preview가 제시되었습니다. 첫 번째 버튼을 클릭해 계정을 팔로우 한 뒤, 해당 tweet을 작성하면 될 것 같네요.

2-2. 계정 팔로우 및 트윗 작성 ✅

첫 번째 버튼을 클릭하면 x에서 자동으로 팔로우 할 것인지에 대한 모달이 나옵니다. 팔로우를 해줍니다.

두 번째 버튼을 클릭하니, 트윗을 복사 붙여넣기 할 필요도 없이 바로 트윗을 포스팅할 수 있도록 화면이 제시되네요. 포스팅을 진행합니다.

2-3. 트윗 URL 제출 ✅

그리고 방금 포스팅 한 글의 URL을 제출했습니다.

2-4. CAPTCHA 인증 ✅

마지막으로 CAPTCHA 인증을 진행했습니다. 그런데 이 단계까지 완료했는데 토큰을 받을 수 없었습니다.

x 계정이 없어서 방금 계정을 생성했고, 팔로우나 트윗 작성과 같은 활동이 없어서 계정의 신뢰도가 낮은 것이 문제였습니다. 20명 정도 팔로우 했더니 해당 모달은 다시 나타나지 않았습니다. 그럼에도 실패했습니다. 원인을 모르겠습니다.

그럼에도 실패의 과정을 기록해야 합니다. 사실 개발이라는 게 99번의 실패 끝에 얻는 한 번의 성공의 맛 때문에 지속할 수 있는 것인데, 99번의 실패를 매번 숨기는 것은 솔직한 개발이 아니라고 생각합니다. 진정한 자존감은 실패를 통과하는 일로부터 얻을 수 있으니까요. 안 되는 게 어딨으셈 ㅋ

자 이제 어떻게 성공했는지 계속 설명해 보겠습니다.


3. Sepolia로 접근하기 🎯

3-1. 문제 재정의 ✅

Monad Testnet 토큰을 얻으려고 했는데 Monad Faucet 서비스에서 한계점이 있었고, Faucet Trade 서비스로 보다 쉽게 Monad Testnet 토큰을 얻으려 했으나 이슈를 극복하지 못했습니다.

이런 아이디어가 떠올랐습니다.

Monad Testnet 토큰을 얻기 어렵다면, 다른 Testnet 토큰을 얻어서 새로운 Testnet에 배포하면 되지 않을까? 그래서 Sepolia를 적용하는 것으로 변경했습니다.

3-2. Sepolia Faucet ✅

https://sepolia-faucet.pk910.de/

위 서비스에서 메타마스크에 있는 ETH 주소를 입력하고 채굴을 시작하면 됩니다. 최소 0.05 SepETH 이상이 되면 채굴을 중단하고 보상을 청구할 수 있습니다.

오후 4시 10분부터 채굴을 진행했습니다. 6분 정도 소요됐네요.

https://sepolia.etherscan.io

Sepolia Testnet Etherscan에서 트랜잭션과 잔액을 확인해 봤습니다.

3-3. 기존 코드 변경 ✅

env PRIVATE_KEY본인의 메타마스크 주소, SEPOLIA_RPC_URLhttps://eth-sepolia.g.alchemy.com/v2/<본인-API-KEY>로 변경해 주세요. API-KEY는 Alchemy에서 얻으실 수 있습니다. 아래 코드들을 참고해 주세요.

hardhat.config.js

js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

const DEFAULT_COMPILER_SETTINGS = {
  version: "0.8.28",
  settings: {
    optimizer: {
      enabled: true,
      runs: 200,
    },
  },
};

module.exports = {
  solidity: DEFAULT_COMPILER_SETTINGS,

  networks: {
    hardhat: {
      chainId: 31337,
    },
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
      chainId: 11155111,
    },
  },
};

contracts/Minkwan.sol

js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract Minkwan is ERC20, Ownable, ERC20Permit {
    constructor(address initialOwner)
        ERC20("Minkwan", "MK")
        Ownable(initialOwner)
        ERC20Permit("Minkwan")
    {}

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

scripts/deploy.js

js
const { ethers } = require("ethers");
const hre = require("hardhat");

async function main() {
  const artifacts = await hre.artifacts.readArtifact("Minkwan");
  const abi = artifacts.abi;
  const bytecode = artifacts.bytecode;

  const provider = new ethers.JsonRpcProvider(process.env.SEPOLIA_RPC_URL);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
  const balance = await provider.getBalance(wallet.address);
  const deployer = wallet.address;

  console.log("Balance: ", balance);
  console.log("Deploying: ", deployer);

  const factory = new ethers.ContractFactory(abi, bytecode, wallet);
  const minkwan = await factory.deploy(deployer);
  await minkwan.waitForDeployment();

  console.log("Deployed to address: ", minkwan.target);
}

main()
  .then(() => process.exit(0))
  .catch((err) => {
    console.error(err);
    process.exit(1);
  });

scripts/mint.js

js
const { ethers } = require("ethers");
const hre = require("hardhat");

async function main() {
  const provider = new ethers.JsonRpcProvider(process.env.SEPOLIA_RPC_URL);
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

  const artifacts = await hre.artifacts.readArtifact("Minkwan");
  const abi = artifacts.abi;

  const minkwan = new ethers.Contract("0x2D33362fe7c5E20106175fa4696a38Fa114F07e4", abi, wallet);
  const mint = await minkwan.mint("0x1fa0E50b32AE8DE76785c6FAC80F14Adbf213391", 1);
  const receipt = await mint.wait();

  console.log(receipt);
  console.log("minted");
}

main()
  .then(() => process.exit(0))
  .catch((err) => {
    console.error(err);
    process.exit(1);
  });

실행 결과

js
(base) wonminkwan@wonmingwan-ui-MacBookAir hardhat % npx hardhat run scripts/deploy.js --network sepolia
[dotenv@17.2.3] injecting env (2) from .env -- tip: 🛠️  run anywhere with `dotenvx run -- yourcommand`
[dotenv@17.2.3] injecting env (0) from .env -- tip: 🔐 encrypt with Dotenvx: https://dotenvx.com
Balance:  55347530000000000n
Deploying:  0x1fa0E50b32AE8DE76785c6FAC80F14Adbf213391
Deployed to address:  0x2D33362fe7c5E20106175fa4696a38Fa114F07e4
(base) wonminkwan@wonmingwan-ui-MacBookAir hardhat % npx hardhat run scripts/mint.js --network sepolia
[dotenv@17.2.3] injecting env (2) from .env -- tip: 🔐 encrypt with Dotenvx: https://dotenvx.com
[dotenv@17.2.3] injecting env (0) from .env -- tip: 🔑 add access controls to secrets: https://dotenvx.com/ops
ContractTransactionReceipt {
  provider: JsonRpcProvider {},
  to: '0x2D33362fe7c5E20106175fa4696a38Fa114F07e4',
  from: '0x1fa0E50b32AE8DE76785c6FAC80F14Adbf213391',
  contractAddress: null,
  hash: '0xda172451af44deab682202eae80eb670a3abde03c6a3a94d95f0e85f978873e7',
  index: 54,
  blockHash: '0xc0eddec0144dd95d235080857d969e3ae4322d37dc75bd65d78c9ce8ab057362',
  blockNumber: 9716397,
  logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000008000000000000000000000000000000000000000000000000020000000000000000000800000000008000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000002200000000000000008000000200000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000',
  gasUsed: 70592n,
  blobGasUsed: null,
  cumulativeGasUsed: 7138569n,
  gasPrice: 4396159n,
  blobGasPrice: null,
  type: 2,
  status: 1,
  root: undefined
}
minted

긴급 추가: Monad도 일단 해결