需求场景说明
两种代币之间的互换,禁止原生代币映射为其他币,但是允许 Token 币映射为原生币,关系图如下。
| From 币 | To 币 | 是否支持 |
|---|---|---|
| ERC20 | ERC20 | ✅ |
| ERC20 | 原生代币 | ✅ |
| 原生代币 | 原生代币 | ❌ |
| 原生代币 | ERC20 | ❌ |
做这个合约的目的:将没有价值的代币,转为有价值的代币。 比如市场做营销的时候,发放的代币积分,或者某个活动合约,得到的活动 Token。可以将这些转为有价值的币。接到的需求是 1:1 映射,但是为了更好的兼容,我在合约内部增加了映射比例的设置口子。这样使用起来更加灵活。
除了上面说的以外,还可以设置稳定币的映射关系。比如设置 USDT --> USDC 和 USDC --> USDT ,每次映射设置 X % 的手续费,这样可以极大的提高资金利用率,如果是主流生态,需要监控 ChainLink 预言机来进行汇率调整,如果是不常用的链,比如 USDT 和 USDC 都是项目方自己做的币,那么直接设置汇率,比如设置收取 0.5% 的手续费,也是完全没问题的。
合约介绍
从下面 5 个方面介绍
- 只读方法
- 构造函数 (部署时候需要的配置)
- 核心方法
- 辅助方法
- 管理方法
- 合约事件
这是一个基于 @openzeppelin/contracts 做的合约,使用了 Ownable/ IERC20/ SafeERC20。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/token/ERC721/IERC721.sol';
/// @title Contract for exchanging ERC20 tokens
/// @author Anbang
/// @notice One-way conversion, irreversible
contract TokenMapping is Ownable {
using SafeERC20 for IERC20;
// ....
}
1 只读方法
mappingStatus: 当前合约的映射状态,只有开启状态的情况下,用户才可以进行映射。true: 开启状态false: 关闭状态;
getPairs: 获取所有映射对- 返回数组,数组内结果如下
fromToken: 0xAAA, toToken: 0xBBB,mappingRates(_from_token,_to_token): 通过 fromToken + toToken 获取汇率- 如果映射汇率为
1e18, 则用户收到的 targetAmount 为user.amount * 1e18 / BASE_MUL - 上面的
BASE_MUL是内部常量,值为1e18
- 如果映射汇率为
pairLength: 合约内的映射对数量。如返回1, 代表有一个映射对pairs(pair_index): 获取指定索引的映射对
TIPS:大家可以根据实际情况修改的地方
- 如果需要记录用户映射金额,可以引入
UserInfo结构体的变量。 - 如果需要记录全局总映射金额,可以引入
PoolInfo结构体的变量。 - 如果打算做基金地址(基金地址用来收取手续费),可以增加
fundAddress变量
2 构造函数
没有 constructor 函数,这个合约比较简单,没有基金地址。如果打算做基金地址,可以在这里进行初始化。
3 核心方法
mappingToken(_from_token,_to_token,_amount): 映射代币- 将
from token兑换为to token,from token的数量为amount - 映射说明
- ✅ 允许 TokenA 映射为 主网币
- ✅ 允许 TokenA 映射为 TokenB
- ❌ 禁止 主网币 映射为 TokenA,主网币不能作为
_from_token
- 当
_to_token为主网币时候,需要传入0x0000000000000000000000000000000000000000
- 将
合约报错说明
- 当收到
Mapping closed错误的时候,此时mappingStatus处在 false 状态: 所有代币不允许进行映射。 - 当收到
Pair doesn't exist错误的时候,此时mappingRates[_from_token][_to_token]为 0,需要添加映射对。 - 当收到
Invalid amount错误的时候,此时传入的_amount为 0,请输入需要映射的数量。 - 当收到
ERC20: transfer amount exceeds allowance错误的时候,此时_from_token为 Token 地址,没有对TokenMapping合约做授权。
4 辅助方法
-
🔒
withdraw(_token_address, _to_address, uint256 _amount): 取出指定token_address到to address- 如果收到错误信息
Invalid to_address: 代表to_address地址为 0 - 如果
_token_address为0x0000000000000000000000000000000000000000表示为提取主网币
- 如果收到错误信息
-
🔒
burn(_token_address, uint256 _amount)- 销毁指定
token_address到address(1) - 该方法不是真正的销毁
- 不支持销毁主网币
- 销毁指定
owner 权限相关的函数,这是来自第三方代码库 '@openzeppelin/contracts/access/Ownable.sol' 中的
transferOwnership: 设置新的 owner 地址,后期转移给 DAO 地址。renounceOwnership: 放弃所有权 (该方法谨慎考虑后才能操作)- 放弃所有权将使合约没有 owner,将无法再调用 onlyOwner 函数。
5 管理方法
- 🔒
addMappingPair(_from_token,_to_token,_mapping_rate): 添加映射对- 当
_to_token为主网币时候,需要传入0x0000000000000000000000000000000000000000 _mapping_rate是映射汇率,基础乘数是1e18(“1000000000000000000”)- ✅✅✅ 重要:
_mapping_rate计算公式为18 - FromDecimals + ToDecimals - 代码内的转换逻辑为:
taretAmount = user.amount * _mapping_rate / 1e18 - From 和 To 币种的精度都一样,且都是
1e18,假设映射前的user.amount = 1e18- 如果是
1:1映射,则传入_mapping_rate应该为1e18taretAmount = 1e18 * 1e18 / 1e18=>1e18
- 如果是
1:0.5映射,则传入_mapping_rate应该为1e18/2taretAmount = 1e18 * 1e18/2 / 1e18=5e17
- 如果是
1:2映射,则传入_mapping_rate应该为1e18*2taretAmount = 1e18 * 1e18*2/ 1e18=2e18
- 如果是
- From 和 To 币种的精度不一样,
- 如果是
1:1映射- FromDecimals 为 18
- ToDecimals 为 8 : 则传入
_mapping_rate为1e8(18-18+8) - ToDecimals 为 6 : 则传入
_mapping_rate为1e6(18-18+6) - ToDecimals 为 2 : 则传入
_mapping_rate为1e2(18-18+2)
- ToDecimals 为 8 : 则传入
- ToDecimals 为 18
- FromDecimals 为 8 : 则传入
_mapping_rate为1e28(18-8+18) - FromDecimals 为 6 : 则传入
_mapping_rate为1e30(18-6+18) - FromDecimals 为 2 : 则传入
_mapping_rate为1e34(18-2+18)
- FromDecimals 为 8 : 则传入
- FromDecimals 为 8
- ToDecimals 为 18 : 则传入
_mapping_rate为1e28(18-8+18) - ToDecimals 为 8 : 则传入
_mapping_rate为1e18(18-8+8) - ToDecimals 为 6 : 则传入
_mapping_rate为1e16(18-8+6) - ToDecimals 为 2 : 则传入
_mapping_rate为1e12(18-8+2)
- ToDecimals 为 18 : 则传入
- FromDecimals 为 18
- 如果是
1:0.5映射,则传入_mapping_rate为(18 - FromDecimals + ToDecimals)/2 - 如果是
1:2映射,则传入_mapping_rate为(18 - FromDecimals + ToDecimals)*2
- 如果是
- ✅✅✅ 重要:
- 如果收到错误信息
Pair already exists: 表示该映射对已经存在了,如果想要修改映射汇率,请使用updatePairRate方法。 - 如果收到错误信息
Invalid from address: 代表_from_token为 0,不支持从主网币映射为 token 币; - 如果收到错误信息
Same address: 代表_from_token和_to_token相同,需要核对地址参数。
- 当
- 🔒
updatePairRate(_from_token,_to_token,_mapping_rate): 更新映射对的映射比例- 如果收到错误信息
Pair doesn't exist;表示该映射对不存在,请使用addMappingPair方法添加。 - 注意: 如果将
_mapping_rate设置为 0,代表关闭该映射对,以后如果再次开启此映射,需要重新添加该添加对。
- 如果收到错误信息
- 🔒
setMappingStatus (bool _status): 设置兑换状态true: 开启状态false: 关闭状态;
6 合约事件
event PairAdd(address indexed from_token, address indexed to_token, uint256 mapping_rate);
event PairUpdate(address indexed from_token, address indexed to_token, uint256 mapping_rate);
event MappingStatus(bool status);
event Withdraw(address indexed token_address, address indexed to_address, uint256 amount);
event Burn(address indexed token_address, uint256 amount);
event MappingToken(
address indexed from_token, address indexed to_token, address indexed to_address,
uint256 source_amount, uint256 target_amount
);
event Error(address caller, bytes data);