메인 콘텐츠로 건너뛰기

사전 요구 사항

  • Plasma 테스트넷에 배포된 컨트랙트. 아직 배포하지 않았다면 컨트랙트 배포하기를 참조하세요.
  • 컨트랙트의 소스 코드와 컴파일 설정.
  • 배포 시 사용된 생성자 인수(있는 경우).
Plasma 블록 익스플로러(plasmascan.to)는 Etherscan 호환 검증 API를 제공합니다. 아래 예시는 해당 API를 직접 사용하며, 검증 제출을 위해 제3자 계정이 필요하지 않습니다.

예시 컨트랙트

단순한 생성자 인수와 복잡한 생성자 인수 모두를 보여주기 위해 두 가지 예시 컨트랙트를 사용하겠습니다:

단순 컨트랙트

스마트 컨트랙트의 기본 개념을 보여주는 기본 데이터 저장 컨트랙트입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract SimpleStorage {
    string private storedData;
    address public owner;
    
    constructor(string memory initialData) {
        storedData = initialData;
        owner = msg.sender;
    }
    
    function setData(string memory data) public {
        require(msg.sender == owner, "Only owner can set data");
        storedData = data;
    }
    
    function getData() public view returns (string memory) {
        return storedData;
    }
}

복잡한 컨트랙트

구성 가능한 매개변수로 토큰 예금을 관리하는 보다 정교한 vault 시스템입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract TokenVault {
    struct VaultConfig {
        uint256 minDeposit;
        uint256 maxDeposit;
        bool isActive;
    }
    
    address[] public authorisedTokens;
    VaultConfig public config;
    mapping(address => uint256) public balances;
    
    constructor(
        address[] memory _tokens,
        uint256 _minDeposit,
        uint256 _maxDeposit,
        bool _isActive
    ) {
        authorisedTokens = _tokens;
        config = VaultConfig(_minDeposit, _maxDeposit, _isActive);
    }
    
    function deposit(address token, uint256 amount) external {
        require(config.isActive, "Vault is not active");
        require(amount >= config.minDeposit, "Amount below minimum");
        require(amount <= config.maxDeposit, "Amount above maximum");
        balances[msg.sender] += amount;
    }
}

Foundry로 검증하기

Foundry는 컨트랙트 검증을 위해 forge verify-contract 명령을 제공합니다.
  1. foundry.toml에 검증 설정이 포함되어 있는지 확인합니다:
    [profile.default]
    src = "src"
    out = "out"
    libs = ["lib"]
    solc_version = "0.8.28"
    optimizer = true
    optimizer_runs = 200
    
    [rpc_endpoints]
    plasma_testnet = "https://testnet-rpc.plasma.to"
    
  2. 컨트랙트를 검증합니다. 문자열 생성자 인수가 있는 SimpleStorage 컨트랙트의 경우:
    forge verify-contract \
      --chain-id 9746 \
      --num-of-optimizations 200 \
      --watch \
      --constructor-args $(cast abi-encode "constructor(string)" "Hello, Plasma!") \
      --compiler-version v0.8.28+commit.7893614a \
      0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
      src/SimpleStorage.sol:SimpleStorage
    
    여러 생성자 인수가 있는 TokenVault 컨트랙트의 경우:
    # 먼저 생성자 인수를 인코딩합니다.
    CONSTRUCTOR_ARGS=$(cast abi-encode \
        "constructor(address[],uint256,uint256,bool)" \
        "[0x1234567890123456789012345678901234567890,0x0987654321098765432109876543210987654321]" \
        1000000000000000000 \
        10000000000000000000 \
        true)
    
    forge verify-contract \
        --chain-id 9746 \
        --num-of-optimizations 200 \
        --watch \
        --constructor-args $CONSTRUCTOR_ARGS \
        --compiler-version v0.8.28+commit.7893614a \
        0x9876543210987654321098765432109876543210 \
        src/TokenVault.sol:TokenVault
    
  3. 검증 상태를 확인합니다. Foundry는 --watch 플래그를 사용하여 실시간 검증 상태를 표시합니다. 다음을 확인하세요:
    Submitting verification for [src/SimpleStorage.sol:SimpleStorage] 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F.
    Submitted contract for verification:
            Response: `OK`
            GUID: `abc123def456ghi789`
            URL: https://testnet.plasmascan.to/address/0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F
    Contract verification status:
    Response: `NOTOK`
    Details: `Pending in queue`
    Contract verification status:
    Response: `OK`
    Details: `Pass - Verified`
    Contract successfully verified
    

Hardhat으로 검증하기

Hardhat@nomicfoundation/hardhat-verify 플러그인을 통해 컨트랙트 검증을 제공합니다.
  1. 검증 설정을 포함하도록 hardhat.config.js를 업데이트합니다:
    require("@nomicfoundation/hardhat-toolbox");
    require("@nomicfoundation/hardhat-verify");
    require("dotenv").config();
    
    module.exports = {
      solidity: {
        version: "0.8.28",
        settings: {
          optimizer: {
            enabled: true,
            runs: 200
          }
        }
      },
      networks: {
        plasmaTestnet: {
          url: process.env.RPC_URL,
          chainId: 9746,
          accounts: [process.env.PRIVATE_KEY]
        }
      },
      etherscan: {
        customChains: [
          {
            network: "plasmaTestnet",
            chainId: 9746,
            urls: {
              apiURL: "https://testnet.plasmascan.to/api",
              browserURL: "https://testnet.plasmascan.to/"
            }
          }
        ]
      },
      sourcify: {
        enabled: false
      }
    };
    
  2. 컨트랙트를 검증합니다. 단순 컨트랙트의 경우:
    npx hardhat verify \
        --network plasmaTestnet \
        0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
        "Hello, Plasma!"
    
    여러 생성자 인수가 있는 컨트랙트의 경우, 검증 스크립트 scripts/verify.js를 생성합니다:
    const hre = require("hardhat");
    
    async function main() {
      const contractAddress = "0x9876543210987654321098765432109876543210";
      
      // TokenVault용 생성자 인수.
      const constructorArgs = [
        [
          "0x1234567890123456789012345678901234567890",
          "0x0987654321098765432109876543210987654321"
        ], // address[] _tokens
        "1000000000000000000", // uint256 _minDeposit (1e18, 18 decimals 가정)
        "10000000000000000000", // uint256 _maxDeposit (1e19, 18 decimals 가정)
        true // bool _isActive
      ];
    
      try {
        await hre.run("verify:verify", {
          address: contractAddress,
          constructorArguments: constructorArgs,
        });
        console.log("Contract verified successfully!");
      } catch (error) {
        console.error("Verification failed:", error);
      }
    }
    
    main()
      .then(() => process.exit(0))
      .catch((error) => {
        console.error(error);
        process.exit(1);
      });
    
  3. 검증 스크립트를 실행합니다:
    npx hardhat run scripts/verify.js --network plasmaTestnet
    

배포 시 자동 검증

배포 스크립트에 검증을 추가하여 배포 중에 컨트랙트를 자동으로 검증할 수도 있습니다:
const { ethers } = require("hardhat");

async function main() {
  const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
  const simpleStorage = await SimpleStorage.deploy("Hello, Plasma!");
  
  await simpleStorage.waitForDeployment();
  const contractAddress = await simpleStorage.getAddress();
  
  console.log("SimpleStorage deployed to:", contractAddress);
  
  // 검증 전에 몇 개의 블록 확인을 기다립니다.
  console.log("Waiting for block confirmations...");
  await simpleStorage.deploymentTransaction().wait(5);
  
  // 컨트랙트를 검증합니다.
  try {
    await hre.run("verify:verify", {
      address: contractAddress,
      constructorArguments: ["Hello, Plasma!"],
    });
    console.log("Contract verified successfully!");
  } catch (error) {
    console.log("Verification failed:", error.message);
  }
}

main();

Ethers.js로 검증하기

Ethers.js는 검증을 위해 블록 익스플로러에 수동으로 API를 호출해야 합니다. 이 방식은 검증 프로세스에 대한 완전한 제어를 제공합니다.
  1. 필요한 종속성을 설치합니다:
    npm install ethers dotenv axios form-data
    
  2. 검증 스크립트 verify-ethers.js를 생성합니다:
    const axios = require('axios');
    const fs = require('fs');
    require('dotenv').config();
    
    async function verifyContract(contractAddress, sourceCode, contractName, constructorArgs = "") {
      const apiUrl = "https://testnet.plasmascan.to/api";
      
      const data = {
        module: 'contract',
        action: 'verifysourcecode',
        contractaddress: contractAddress,
        sourceCode: sourceCode,
        codeformat: 'solidity-single-file',
        contractname: contractName,
        compilerversion: 'v0.8.28+commit.7893614a',
        optimizationUsed: '1',
        runs: '200',
        constructorArguements: constructorArgs, // 참고: API는 이 철자를 사용합니다.
        evmversion: 'default',
        licenseType: '3' // MIT License
      };
    
      try {
        console.log('Submitting contract for verification...');
        const response = await axios.post(apiUrl, new URLSearchParams(data));
        
        if (response.data.status === '1') {
          const guid = response.data.result;
          console.log('Verification submitted successfully!');
          console.log('GUID:', guid);
          
          // 검증 상태를 확인합니다.
          await checkVerificationStatus(guid);
        } else {
          console.error('Verification submission failed:', response.data.result);
        }
      } catch (error) {
        console.error('Error submitting verification:', error.message);
      }
    }
    
    async function checkVerificationStatus(guid) {
      const apiUrl = "https://testnet.plasmascan.to/api";
      const maxAttempts = 30;
      let attempts = 0;
    
      while (attempts < maxAttempts) {
        try {
          const response = await axios.get(apiUrl, {
            params: {
              module: 'contract',
              action: 'checkverifystatus',
              guid: guid
            }
          });
    
          const status = response.data.status;
          const result = response.data.result;
    
          if (status === '1') {
            console.log('✅ Contract verified successfully!');
            console.log('Result:', result);
            break;
          } else if (result.includes('Fail')) {
            console.error('❌ Verification failed:', result);
            break;
          } else {
            console.log('⏳ Verification pending...');
            attempts++;
            
            if (attempts < maxAttempts) {
              await new Promise(resolve => setTimeout(resolve, 10000)); // 10초 대기.
            }
          }
        } catch (error) {
          console.error('Error checking status:', error.message);
          break;
        }
      }
    
      if (attempts >= maxAttempts) {
        console.log('⚠️ Verification status check timed out. Please check manually.');
      }
    }
    
    // SimpleStorage 컨트랙트를 검증합니다.
    async function verifySimpleStorage() {
      const contractAddress = "0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F";
      const sourceCode = fs.readFileSync('SimpleStorage.sol', 'utf8');
      const constructorArgs = "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20506c61736d612100000000000000000000000000000000000000";
      
      await verifyContract(contractAddress, sourceCode, "SimpleStorage", constructorArgs);
    }
    
    // TokenVault 컨트랙트를 검증합니다.
    async function verifyTokenVault() {
      const contractAddress = "0x9876543210987654321098765432109876543210";
      const sourceCode = fs.readFileSync('TokenVault.sol', 'utf8');
      
      // 복잡한 생성자 인수 (ABI 인코딩).
      const constructorArgs = "0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000123456789012345678901234567890123456789000000000000000000000000098765432109876543210987654321098765432100";
      
      await verifyContract(contractAddress, sourceCode, "TokenVault", constructorArgs);
    }
    
    // 검증 실행 (필요한 것의 주석을 해제하세요).
    verifySimpleStorage();
    // verifyTokenVault();
    
  3. 생성자 인수를 인코딩합니다. 복잡한 생성자 인수의 경우 ABI 인코딩이 필요합니다. Foundry의 cast 도구를 사용할 수 있습니다:
    # 문자열 인수가 있는 SimpleStorage용.
    cast abi-encode "constructor(string)" "Hello, Plasma!"
    
    # 복잡한 인수가 있는 TokenVault용.
    cast abi-encode \
        "constructor(address[],uint256,uint256,bool)" \
        "[0x1234567890123456789012345678901234567890,0x0987654321098765432109876543210987654321]" \
        1000000000000000000 \
        10000000000000000000 \
        true
    
  4. 마지막으로 검증을 실행합니다:
    node verify-ethers.js
    

검증 상태 확인

검증 후 성공 여부를 확인할 수 있습니다:

블록 익스플로러에서

  1. Plasma 테스트넷 익스플로러를 방문합니다.
  2. 컨트랙트 주소를 검색합니다.
  3. “Contract” 탭 옆의 녹색 체크 표시를 확인합니다.
  4. “Contract” 탭을 클릭하여 검증된 소스 코드를 봅니다.

프로그래밍 방식

  1. 상태 확인 스크립트 check-verification.js를 생성합니다:
    const axios = require('axios');
    require('dotenv').config();
    
    async function checkIfVerified(contractAddress) {
      const apiUrl = "https://testnet.plasmascan.to/api";
      
      try {
        const response = await axios.get(apiUrl, {
          params: {
            module: 'contract',
            action: 'getsourcecode',
            address: contractAddress,
          }
        });
    
        const result = response.data.result[0];
        
        if (result.SourceCode && result.SourceCode !== '') {
          console.log('✅ Contract is verified!');
          console.log('Contract Name:', result.ContractName);
          console.log('Compiler Version:', result.CompilerVersion);
          console.log('Optimisation Used:', result.OptimizationUsed);
          return true;
        } else {
          console.log('❌ Contract is not verified');
          return false;
        }
      } catch (error) {
        console.error('Error checking verification status:', error.message);
        return false;
      }
    }
    
    // 컨트랙트를 확인합니다.
    checkIfVerified("0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F");
    

대체 방법

블록 익스플로러가 가장 일반적인 검증 방법이지만 다음을 사용할 수도 있습니다:

Sourcify

Sourcify는 분산형 컨트랙트 검증을 제공합니다:
# Sourcify CLI 설치.
npm install -g @ethereum-sourcify/cli

# 컨트랙트 검증.
sourcify verify \
    --network 9746 \
    --address 0x742d35Cc6610C7532C8582d4C371Acb1D5f44D7F \
    --files contracts/SimpleStorage.sol

Tenderly

Tenderly는 디버깅 플랫폼의 일부로 검증을 제공합니다. 자세한 내용은 문서를 참조하세요.

컨트랙트 평탄화

임포트 또는 라이브러리가 있는 컨트랙트의 경우 검증 전에 컨트랙트를 단일 파일로 평탄화해야 할 수 있습니다. 인기 있는 도구는 다음과 같습니다: 외부 라이브러리가 있는 복잡한 프로젝트의 경우 고급 구성 옵션은 Hardhat 검증 문서를 참조하세요.

문제 해결

생성자 인수 불일치

Error: Invalid constructor arguments provided
생성자 인수가 배포 시 사용된 것과 정확히 일치하는지 다시 확인하세요. cast abi-encode를 사용하여 인코딩을 검증할 수 있습니다.

컴파일러 버전 불일치

Error: Compilation failed
검증 요청의 컴파일러 버전이 컴파일 시 사용된 버전과 일치하는지 확인하세요.

최적화 설정 불일치

Error: Bytecode doesn't match
최적화 설정(활성화/비활성화 및 runs 수)이 컴파일 설정과 일치하는지 확인하세요.

이미 검증됨

Error: Contract source code already verified
이는 이전에 검증이 성공했음을 의미합니다. 블록 익스플로러에서 확인하세요.

속도 제한

Error: Rate limit exceeded
몇 분 후에 다시 시도하세요. 더 높은 한도가 필요하다면 API 플랜 업그레이드를 고려하세요.