Solidity多语言支持:让你的合约会说“全球话”,用户体验直接拉满!

搞区块链开发,合约要是只能说一种语言,那用户不得抓瞎?全球玩家一堆,英语、中文、俄文、韩文全得招呼上!Solidity里实现多语言支持,听起来高大上,其实就是让合约动态吐出不同语言的提示、错误信息,或者接口描述,服务全球用户。多语言支持的核心套路Solidity里搞多语言支持,核心是把不同语言的

搞区块链开发,合约要是只能说一种语言,那用户不得抓瞎?全球玩家一堆,英语、中文、俄文、韩文全得招呼上!Solidity里实现多语言支持,听起来高大上,其实就是让合约动态吐出不同语言的提示、错误信息,或者接口描述,服务全球用户。

多语言支持的核心套路

Solidity里搞多语言支持,核心是把不同语言的字符串存下来,根据用户选择的语言代码(比如enzh)动态返回对应文本。EVM没内置国际化库,字符串得存进存储(mapping或数组),用bytes32string存文本。bytes32省Gas但长度有限(32字节,英文OK,中文吃力),string灵活但存储成本高。语言切换靠用户传入语言代码,合约查mapping返回对应文本。错误信息、事件描述、甚至前端显示的提示都能多语言化。

关键点:存储设计得高效,SSTORE/SLOAD成本高,尽量用bytes32或静态数组。语言代码用ISO 639-1标准(如enzh),映射到文本。权限控制得加,防止恶意修改语言数据。可升级合约支持动态添加语言。事件记录语言切换,方便链上查。

Gas上,存储string单条20k Gas,bytes3210k。读取~2k。批量初始化多语言数据得控制规模,防Gas超限。拿ERC20合约举例:transfer的错误信息可以动态返回Insufficient balance(英语)或余额不足(中文)。

开发环境先搭好

用Hardhat建环境,写合约和测试,测Gas成本。

跑命令初始化:

mkdir i18n-demo
cd i18n-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @openzeppelin/contracts-upgradeable
npm install ethers
npx hardhat init

选TypeScript,装依赖:

npm install --save-dev ts-node typescript @types/node @types/mocha

目录结构:

i18n-demo/
├── contracts/
│   ├── BasicI18n.sol
│   ├── MappingI18n.sol
│   ├── DynamicI18n.sol
│   ├── UpgradableI18n.sol
├── scripts/
│   ├── deploy.ts
├── test/
│   ├── I18n.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json

tsconfig.json配置:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "outDir": "./dist",
    "rootDir": "./"
  },
  "include": ["hardhat.config.ts", "scripts", "test"]
}

hardhat.config.ts配置:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    hardhat: {
      chainId: 1337,
    },
  },
};

export default config;

启动节点:

npx hardhat node

环境OK,直接上代码!

基础多语言:静态字符串数组

先搞个简单版,用数组存多语言错误信息,语言固定。

代码实操

contracts/BasicI18n.sol

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

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

contract BasicI18nToken is ERC20, Ownable {
    string[3] public languages = ["en", "zh", "es"];
    mapping(string => mapping(string => string)) public messages;

    event LanguageSet(address indexed user, string language);
    event TransferFailed(address indexed from, address indexed to, uint256 amount, string message);

    constructor() ERC20("BasicI18nToken", "BIT") Ownable() {
        _mint(msg.sender, 1000000 * 10**decimals());
        // English
        messages["en"]["insufficient_balance"] = "Insufficient balance";
        messages["en"]["invalid_recipient"] = "Invalid recipient";
        // Chinese
        messages["zh"]["insufficient_balance"] = "\xe4\xbd\x99\xe9\xa2\x9d\xe4\xb8\x8d\xe8\xb6\xb3"; // 余额不足
        messages["zh"]["invalid_recipient"] = "\xe6\x97\xa0\xe6\x95\x88\xe6\x8e\xa5\xe6\x94\xb6\xe8\x80\x85"; // 无效接收者
        // Spanish
        messages["es"]["insufficient_balance"] = "Saldo insuficiente";
        messages["es"]["invalid_recipient"] = "Destinatario invalido";
    }

    function setLanguage(string memory language) public {
        bool valid = false;
        for (uint256 i = 0; i < languages.length; i++) {
            if (keccak256(abi.encodePacked(languages[i])) == keccak256(abi.encodePacked(language))) {
                valid = true;
                break;
            }
        }
        require(valid, "Unsupported language");
        emit LanguageSet(msg.sender, language);
    }

    function transfer(address to, uint256 amount, string memory language) public returns (bool) {
        if (to == address(0)) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["invalid_recipient"]);
            revert(messages[language]["invalid_recipient"]);
        }
        if (balanceOf(msg.sender) < amount) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["insufficient_balance"]);
            revert(messages[language]["insufficient_balance"]);
        }
        return super.transfer(to, amount);
    }
}

代码解析

  • 核心逻辑
    • languages数组存支持的语言代码(en, zh, es)。
    • messages映射:language -> key -> message
    • setLanguage:验证语言代码,触发事件。
    • transfer:根据语言返回对应错误信息。
    • 构造函数:初始化英语、中文、西班牙语错误信息(中文用UTF-8编码)。
  • 安全点
    • 语言代码校验,防无效输入。
    • 事件记录语言选择和失败原因。
    • string存储支持任意语言。
  • 潜在问题
    • 构造函数初始化多语言,Gas成本高(每string~20k Gas)。
    • 语言固定,扩展麻烦。
  • Gas成本
    • 部署:~600k Gas(含6条string存储)。
    • setLanguage:~5k Gas。
    • transfer失败:~10k Gas(含事件)。

测试

test/I18n.test.ts

import { ethers } from "hardhat";
import { expect } from "chai";
import { BasicI18nToken } from "../typechain-types";

describe("BasicI18n", function () {
  let token: BasicI18nToken;
  let owner: any, user1: any;

  beforeEach(async function () {
    [owner, user1] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("BasicI18nToken");
    token = await TokenFactory.deploy();
    await token.deployed();
  });

  it("sets valid language", async function () {
    await expect(token.setLanguage("en")).to.emit(token, "LanguageSet").withArgs(owner.address, "en");
    await expect(token.setLanguage("zh")).to.emit(token, "LanguageSet").withArgs(owner.address, "zh");
  });

  it("reverts on invalid language", async function () {
    await expect(token.setLanguage("fr")).to.be.revertedWith("Unsupported language");
  });

  it("returns language-specific errors", async function () {
    await expect(token.transfer(ethers.constants.AddressZero, ethers.utils.parseEther("500"), "en"))
      .to.be.revertedWith("Invalid recipient");
    await expect(token.transfer(ethers.constants.AddressZero, ethers.utils.parseEther("500"), "zh"))
      .to.be.revertedWith("\xe6\x97\xa0\xe6\x95\x88\xe6\x8e\xa5\xe6\x94\xb6\xe8\x80\x85");
    await token.transfer(user1.address, ethers.utils.parseEther("1000"), "en");
    await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("2000"), "es"))
      .to.be.revertedWith("Saldo insuficiente");
  });
});

跑测试:

npx hardhat test
  • 测试解析
    • 语言设置成功,事件触发。
    • 无效语言失败。
    • 不同语言的错误信息正确返回。
  • 基础:静态数组简单,但扩展性差。

动态映射:灵活多语言

静态数组不灵活,搞个动态映射,支持随时加语言。

代码实操

contracts/MappingI18n.sol

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

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

contract MappingI18nToken is ERC20, Ownable {
    mapping(string => bool) public supportedLanguages;
    mapping(string => mapping(string => bytes32)) public messages;

    event LanguageAdded(string language);
    event MessageUpdated(string language, string key, bytes32 message);
    event LanguageSet(address indexed user, string language);
    event TransferFailed(address indexed from, address indexed to, uint256 amount, bytes32 message);

    constructor() ERC20("MappingI18nToken", "MIT") Ownable() {
        _mint(msg.sender, 1000000 * 10**decimals());
        supportedLanguages["en"] = true;
        supportedLanguages["zh"] = true;
        messages["en"]["insufficient_balance"] = bytes32("Insufficient balance");
        messages["en"]["invalid_recipient"] = bytes32("Invalid recipient");
        messages["zh"]["insufficient_balance"] = bytes32("\xe4\xbd\x99\xe9\xa2\x9d\xe4\xb8\x8d\xe8\xb6\xb3");
        messages["zh"]["invalid_recipient"] = bytes32("\xe6\x97\xa0\xe6\x95\x88\xe6\x8e\xa5\xe6\x94\xb6\xe8\x80\x85");
    }

    function addLanguage(string memory language) public onlyOwner {
        require(!supportedLanguages[language], "Language exists");
        supportedLanguages[language] = true;
        emit LanguageAdded(language);
    }

    function updateMessage(string memory language, string memory key, bytes32 message) public onlyOwner {
        require(supportedLanguages[language], "Unsupported language");
        messages[language][key] = message;
        emit MessageUpdated(language, key, message);
    }

    function setLanguage(string memory language) public {
        require(supportedLanguages[language], "Unsupported language");
        emit LanguageSet(msg.sender, language);
    }

    function transfer(address to, uint256 amount, string memory language) public returns (bool) {
        require(supportedLanguages[language], "Unsupported language");
        if (to == address(0)) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["invalid_recipient"]);
            revert(string(abi.encodePacked(messages[language]["invalid_recipient"])));
        }
        if (balanceOf(msg.sender) < amount) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["insufficient_balance"]);
            revert(string(abi.encodePacked(messages[language]["insufficient_balance"])));
        }
        return super.transfer(to, amount);
    }
}

代码解析

  • 核心逻辑
    • supportedLanguages:映射标记支持语言。
    • messages:用bytes32存消息,省Gas。
    • addLanguage:动态加语言,onlyOwner
    • updateMessage:更新语言消息。
    • transfer:返回bytes32string的错误信息。
  • 安全点
    • onlyOwner控制语言和消息更新。
    • bytes32限制长度,防溢出。
    • 事件记录语言和消息变更。
  • 潜在 problem
    • bytes32不支持长文本。
    • 动态添加语言增加SSTORE。
  • Gas成本
    • 部署:~550k Gas。
    • addLanguage:~20k Gas。
    • updateMessage:~15k Gas。
    • transfer失败:~8k Gas。

测试

test/I18n.test.ts(add):

import { MappingI18nToken } from "../typechain-types";

describe("MappingI18n", function () {
  let token: MappingI18nToken;
  let owner: any, user1: any;

  beforeEach(async function () {
    [owner, user1] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("MappingI18nToken");
    token = await TokenFactory.deploy();
    await token.deployed();
  });

  it("adds new language", async function () {
    await expect(token.addLanguage("es"))
      .to.emit(token, "LanguageAdded")
      .withArgs("es");
    await token.updateMessage("es", "insufficient_balance", ethers.utils.formatBytes32String("Saldo insuficiente"));
    await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("2000"), "es"))
      .to.be.revertedWith("Saldo insuficiente");
  });

  it("updates message", async function () {
    await expect(token.updateMessage("en", "insufficient_balance", ethers.utils.formatBytes32String("Not enough balance")))
      .to.emit(token, "MessageUpdated")
      .withArgs("en", "insufficient_balance", ethers.utils.formatBytes32String("Not enough balance"));
    await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("2000"), "en"))
      .to.be.revertedWith("Not enough balance");
  });

  it("blocks invalid language", async function () {
    await expect(token.transfer(owner.address, ethers.utils.parseEther("500"), "fr"))
      .to.be.revertedWith("Unsupported language");
  });
});
  • 测试解析
    • 添加语言和消息成功。
    • 动态语言错误信息生效。
    • 无效语言失败。
  • 优势:动态语言支持,扩展灵活。

动态语言切换:用户自定义语言

让用户动态切换语言,存用户偏好。

代码实操

contracts/DynamicI18n.sol

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract DynamicI18nToken is ERC20, Ownable, ReentrancyGuard {
    mapping(string => bool) public supportedLanguages;
    mapping(string => mapping(string => bytes32)) public messages;
    mapping(address => string) public userLanguages;
    string public defaultLanguage = "en";

    event LanguageAdded(string language);
    event MessageUpdated(string language, string key, bytes32 message);
    event UserLanguageSet(address indexed user, string language);
    event TransferFailed(address indexed from, address indexed to, uint256 amount, bytes32 message);

    constructor() ERC20("DynamicI18nToken", "DIT") Ownable() {
        _mint(msg.sender, 1000000 * 10**decimals());
        supportedLanguages["en"] = true;
        supportedLanguages["zh"] = true;
        messages["en"]["insufficient_balance"] = bytes32("Insufficient balance");
        messages["en"]["invalid_recipient"] = bytes32("Invalid recipient");
        messages["zh"]["insufficient_balance"] = bytes32("\xe4\xbd\x99\xe9\xa2\x9d\xe4\xb8\x8d\xe8\xb6\xb3");
        messages["zh"]["invalid_recipient"] = bytes32("\xe6\x97\xa0\xe6\x95\x88\xe6\x8e\xa5\xe6\x94\xb6\xe8\x80\x85");
    }

    function addLanguage(string memory language) public onlyOwner {
        require(!supportedLanguages[language], "Language exists");
        supportedLanguages[language] = true;
        emit LanguageAdded(language);
    }

    function updateMessage(string memory language, string memory key, bytes32 message) public onlyOwner {
        require(supportedLanguages[language], "Unsupported language");
        messages[language][key] = message;
        emit MessageUpdated(language, key, message);
    }

    function setUserLanguage(string memory language) public nonReentrant {
        require(supportedLanguages[language], "Unsupported language");
        userLanguages[msg.sender] = language;
        emit UserLanguageSet(msg.sender, language);
    }

    function transfer(address to, uint256 amount) public nonReentrant returns (bool) {
        string memory language = bytes(userLanguages[msg.sender]).length == 0 ? defaultLanguage : userLanguages[msg.sender];
        if (to == address(0)) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["invalid_recipient"]);
            revert(string(abi.encodePacked(messages[language]["invalid_recipient"])));
        }
        if (balanceOf(msg.sender) < amount) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["insufficient_balance"]);
            revert(string(abi.encodePacked(messages[language]["insufficient_balance"])));
        }
        return super.transfer(to, amount);
    }
}

代码解析

  • 核心逻辑
    • userLanguages:存用户语言偏好。
    • setUserLanguage:用户设置语言,防重入。
    • transfer:优先用用户语言,无则用默认en
    • bytes32存消息,省Gas。
  • 安全点
    • nonReentrant防重入。
    • 默认语言兜底,防空语言。
    • 事件记录用户语言变更。
  • 潜在问题
    • 用户语言存储增加SSTORE(~20k Gas)。
    • bytes32长度限制。
  • Gas成本
    • 部署:~600k Gas。
    • setUserLanguage:~25k Gas。
    • transfer失败:~10k Gas。

测试

test/I18n.test.ts(add):

import { DynamicI18nToken } from "../typechain-types";

describe("DynamicI18n", function () {
  let token: DynamicI18nToken;
  let owner: any, user1: any;

  beforeEach(async function () {
    [owner, user1] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("DynamicI18nToken");
    token = await TokenFactory.deploy();
    await token.deployed();
  });

  it("sets user language", async function () {
    await expect(token.connect(user1).setUserLanguage("zh"))
      .to.emit(token, "UserLanguageSet")
      .withArgs(user1.address, "zh");
    await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("2000")))
      .to.be.revertedWith("\xe4\xbd\x99\xe9\xa2\x9d\xe4\xb8\x8d\xe8\xb6\xb3");
  });

  it("uses default language", async function () {
    await expect(token.connect(user1).transfer(owner.address, ethers.utils.parseEther("2000")))
      .to.be.revertedWith("Insufficient balance");
  });

  it("blocks invalid user language", async function () {
    await expect(token.connect(user1).setUserLanguage("fr"))
      .to.be.revertedWith("Unsupported language");
  });
});
  • 测试解析
    • 用户语言设置成功,错误信息匹配。
    • 默认语言en生效。
    • 无效语言失败。
  • 优势:用户自定义语言,体验更好。

可升级多语言支持

语言支持得能扩展,搞可升级合约,动态加语言。

代码实操

contracts/UpgradableI18n.sol

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";

contract UpgradableI18nToken is ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable {
    mapping(string => bool) public supportedLanguages;
    mapping(string => mapping(string => bytes32)) public messages;
    mapping(address => string) public userLanguages;
    string public defaultLanguage;

    event LanguageAdded(string language);
    event MessageUpdated(string language, string key, bytes32 message);
    event UserLanguageSet(address indexed user, string language);
    event TransferFailed(address indexed from, address indexed to, uint256 amount, bytes32 message);

    function initialize() public initializer {
        __ERC20_init("UpgradableI18nToken", "UIT");
        __Ownable_init();
        __UUPSUpgradeable_init();
        __ReentrancyGuard_init();
        _mint(msg.sender, 1000000 * 10**decimals());
        supportedLanguages["en"] = true;
        supportedLanguages["zh"] = true;
        defaultLanguage = "en";
        messages["en"]["insufficient_balance"] = bytes32("Insufficient balance");
        messages["en"]["invalid_recipient"] = bytes32("Invalid recipient");
        messages["zh"]["insufficient_balance"] = bytes32("\xe4\xbd\x99\xe9\xa2\x9d\xe4\xb8\x8d\xe8\xb6\xb3");
        messages["zh"]["invalid_recipient"] = bytes32("\xe6\x97\xa0\xe6\x95\x88\xe6\x8e\xa5\xe6\x94\xb6\xe8\x80\x85");
    }

    function addLanguage(string memory language) public onlyOwner {
        require(!supportedLanguages[language], "Language exists");
        supportedLanguages[language] = true;
        emit LanguageAdded(language);
    }

    function updateMessage(string memory language, string memory key, bytes32 message) public onlyOwner {
        require(supportedLanguages[language], "Unsupported language");
        messages[language][key] = message;
        emit MessageUpdated(language, key, message);
    }

    function setUserLanguage(string memory language) public nonReentrant {
        require(supportedLanguages[language], "Unsupported language");
        userLanguages[msg.sender] = language;
        emit UserLanguageSet(msg.sender, language);
    }

    function transfer(address to, uint256 amount) public nonReentrant returns (bool) {
        string memory language = bytes(userLanguages[msg.sender]).length == 0 ? defaultLanguage : userLanguages[msg.sender];
        if (to == address(0)) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["invalid_recipient"]);
            revert(string(abi.encodePacked(messages[language]["invalid_recipient"])));
        }
        if (balanceOf(msg.sender) < amount) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["insufficient_balance"]);
            revert(string(abi.encodePacked(messages[language]["insufficient_balance"])));
        }
        return super.transfer(to, amount);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

contracts/UpgradableI18nV2.sol

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";

contract UpgradableI18nTokenV2 is ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable, ReentrancyGuardUpgradeable {
    mapping(string => bool) public supportedLanguages;
    mapping(string => mapping(string => bytes32)) public messages;
    mapping(address => string) public userLanguages;
    string public defaultLanguage;
    uint256 public languageFee; // Fee for setting language

    event LanguageAdded(string language);
    event MessageUpdated(string language, string key, bytes32 message);
    event UserLanguageSet(address indexed user, string language);
    event TransferFailed(address indexed from, address indexed to, uint256 amount, bytes32 message);

    function initialize() public initializer {
        __ERC20_init("UpgradableI18nTokenV2", "UITV2");
        __Ownable_init();
        __UUPSUpgradeable_init();
        __ReentrancyGuard_init();
        _mint(msg.sender, 1000000 * 10**decimals());
        supportedLanguages["en"] = true;
        supportedLanguages["zh"] = true;
        defaultLanguage = "en";
        languageFee = 1 * 10**15; // 0.001 token fee
        messages["en"]["insufficient_balance"] = bytes32("Insufficient balance");
        messages["en"]["invalid_recipient"] = bytes32("Invalid recipient");
        messages["zh"]["insufficient_balance"] = bytes32("\xe4\xbd\x99\xe9\xa2\x9d\xe4\xb8\x8d\xe8\xb6\xb3");
        messages["zh"]["invalid_recipient"] = bytes32("\xe6\x97\xa0\xe6\x95\x88\xe6\x8e\xa5\xe6\x94\xb6\xe8\x80\x85");
    }

    function addLanguage(string memory language) public onlyOwner {
        require(!supportedLanguages[language], "Language exists");
        supportedLanguages[language] = true;
        emit LanguageAdded(language);
    }

    function updateMessage(string memory language, string memory key, bytes32 message) public onlyOwner {
        require(supportedLanguages[language], "Unsupported language");
        messages[language][key] = message;
        emit MessageUpdated(language, key, message);
    }

    function setUserLanguage(string memory language) public nonReentrant {
        require(supportedLanguages[language], "Unsupported language");
        require(balanceOf(msg.sender) >= languageFee, "Insufficient balance for fee");
        _burn(msg.sender, languageFee);
        userLanguages[msg.sender] = language;
        emit UserLanguageSet(msg.sender, language);
    }

    function transfer(address to, uint256 amount) public nonReentrant returns (bool) {
        string memory language = bytes(userLanguages[msg.sender]).length == 0 ? defaultLanguage : userLanguages[msg.sender];
        if (to == address(0)) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["invalid_recipient"]);
            revert(string(abi.encodePacked(messages[language]["invalid_recipient"])));
        }
        if (balanceOf(msg.sender) < amount) {
            emit TransferFailed(msg.sender, to, amount, messages[language]["insufficient_balance"]);
            revert(string(abi.encodePacked(messages[language]["insufficient_balance"])));
        }
        return super.transfer(to, amount);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

contracts/UUPSProxy.sol

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

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract UUPSProxy is ERC1967Proxy {
    constructor(address logic, bytes memory data) ERC1967Proxy(logic, data) {}
}

代码解析

  • 核心逻辑
    • UpgradableI18nToken:UUPS代理,支持多语言。
    • initialize:初始化代币、语言、消息。
    • setUserLanguage:用户设置语言,V2加费用。
    • UpgradableI18nTokenV2:新增languageFee,保持存储布局。
  • 安全点
    • UUPS确保升级安全。
    • nonReentrant防重入。
    • 事件记录语言变更。
  • 潜在问题
    • 升级需保持存储一致。
    • 费用机制需平衡用户体验。
  • Gas成本
    • 部署:~800k Gas(含代理)。
    • setUserLanguage:~30k Gas(含费)。
    • transfer失败:~10k Gas。

测试

test/I18n.test.ts(add):

import { UUPSProxy, UpgradableI18nToken, UpgradableI18nTokenV2 } from "../typechain-types";

describe("UpgradableI18n", function () {
  let proxy: UUPSProxy;
  let token: UpgradableI18nToken;
  let tokenV2: UpgradableI18nTokenV2;
  let owner: any, user1: any;

  beforeEach(async function () {
    [owner, user1] = await ethers.getSigners();
    const TokenFactory = await ethers.getContractFactory("UpgradableI18nToken");
    token = await TokenFactory.deploy();
    await token.deployed();

    const ProxyFactory = await ethers.getContractFactory("UUPSProxy");
    const initData = TokenFactory.interface.encodeFunctionData("initialize");
    proxy = await ProxyFactory.deploy(token.address, initData);
    await proxy.deployed();

    const TokenV2Factory = await ethers.getContractFactory("UpgradableI18nTokenV2");
    tokenV2 = await TokenV2Factory.deploy();
    await tokenV2.deployed();
  });

  it("sets user language", async function () {
    const proxyAsToken = await ethers.getContractFactory("UpgradableI18nToken").then(f => f.attach(proxy.address));
    await proxyAsToken.connect(user1).setUserLanguage("zh");
    await expect(proxyAsToken.connect(user1).transfer(owner.address, ethers.utils.parseEther("2000")))
      .to.be.revertedWith("\xe4\xbd\x99\xe9\xa2\x9d\xe4\xb8\x8d\xe8\xb6\xb3");
  });

  it("upgrades and applies language fee", async function () {
    const proxyAsToken = await ethers.getContractFactory("UpgradableI18nToken").then(f => f.attach(proxy.address));
    await proxyAsToken.upgradeTo(tokenV2.address);
    const proxyAsTokenV2 = await ethers.getContractFactory("UpgradableI18nTokenV2").then(f => f.attach(proxy.address));
    await proxyAsTokenV2.transfer(user1.address, ethers.utils.parseEther("1000"));
    await expect(proxyAsTokenV2.connect(user1).setUserLanguage("zh"))
      .to.emit(proxyAsTokenV2, "UserLanguageSet")
      .withArgs(user1.address, "zh");
    expect(await proxyAsTokenV2.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("999"));
  });
});
  • 测试解析
    • 用户语言设置成功。
    • 升级后收费生效,余额扣减。
  • 优势:可升级支持新语言,灵活性高。

部署脚本

scripts/deploy.ts

import { ethers } from "hardhat";

async function main() {
  const [owner] = await ethers.getSigners();

  const BasicI18nFactory = await ethers.getContractFactory("BasicI18nToken");
  const basicI18n = await BasicI18nFactory.deploy();
  await basicI18n.deployed();
  console.log(`BasicI18nToken deployed to: ${basicI18n.address}`);

  const MappingI18nFactory = await ethers.getContractFactory("MappingI18nToken");
  const mappingI18n = await MappingI18nFactory.deploy();
  await mappingI18n.deployed();
  console.log(`MappingI18nToken deployed to: ${mappingI18n.address}`);

  const DynamicI18nFactory = await ethers.getContractFactory("DynamicI18nToken");
  const dynamicI18n = await DynamicI18nFactory.deploy();
  await dynamicI18n.deployed();
  console.log(`DynamicI18nToken deployed to: ${dynamicI18n.address}`);

  const UpgradableI18nFactory = await ethers.getContractFactory("UpgradableI18nToken");
  const upgradableI18n = await UpgradableI18nFactory.deploy();
  await upgradableI18n.deployed();
  const initData = UpgradableI18nFactory.interface.encodeFunctionData("initialize");
  const ProxyFactory = await ethers.getContractFactory("UUPSProxy");
  const proxy = await ProxyFactory.deploy(upgradableI18n.address, initData);
  await proxy.deployed();
  console.log(`UpgradableI18nToken deployed to: ${upgradableI18n.address}, Proxy: ${proxy.address}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

跑部署:

npx hardhat run scripts/deploy.ts --network hardhat

多语言审计清单

  • 存储设计
    • bytes32省Gas,string灵活。
    • 控制语言数量,防Gas超限。
  • 权限控制
    • onlyOwner锁语言和消息更新。
  • 安全校验
    • 验证语言代码和消息长度。
    • 防重入保护用户设置。
  • 事件记录
    • 语言添加、消息更新、用户选择。
  • 测试覆盖
    • 测试语言设置、错误信息、升级。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!