需求场景说明
两种代币之间的互换,禁止原生代币映射为其他币,但是允许 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
应该为1e18
taretAmount = 1e18 * 1e18 / 1e18
=>1e18
- 如果是
1:0.5
映射,则传入_mapping_rate
应该为1e18/2
taretAmount = 1e18 * 1e18/2 / 1e18
=5e17
- 如果是
1:2
映射,则传入_mapping_rate
应该为1e18*2
taretAmount = 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);