结合交易页面与源码看1INCH的兑换逻辑

1INCH的主要优缺点

优点

  1. 支持主流的DEX,计算的兑换路径比较
  2. 支持用户自定义允许的最大划点、交易速度(gas费价格)

缺点

  1. 在计算好兑换路径之后,可能会因为兑换链路过长而导致价格已经变化;此时会回滚交易,浪费了gas费又没有成交
  2. 在交易时如果gas费没有用完,会归1INCH合约所有而不是返还给调用者。

1INCH的主要逻辑

总结成一句话是在以太坊网络上查询其他的去中心化交易所的兑换汇率,然后通过拆分或者过渡令牌交换的方式选择最佳的兑换方式。

两种兑换方式

过渡令牌

如下图ETH/USDT的交易对,1inch算出的最佳方案是先兑换成sUSD,再用sUSD兑换WETH,再用WETH兑换ETH。

image-20210415160725165

拆分

下面的SNX/USDT交易对的图显示先用过渡令牌,最后一步时拆分到两个交易所兑换SNX。

SNX:USDT拆分

目前支持的DEX

背后的逻辑

代币源码:1INCH

  • 是一个ERC20代币
  • 总共发行15亿枚
  • 可以被销毁(必须由持有者允许)

合约代码如下,没有其他的特殊性

1
2
3
4
5
6
7
8
9
10
11

contract OneInch is ERC20Permit, ERC20Burnable, Ownable {
constructor(address _owner) public ERC20("1INCH Token", "1INCH") EIP712("1INCH Token", "1") {
_mint(_owner, 1.5e9 ether);
transferOwnership(_owner);
}

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

协议源码:1inchProtocol

此协议描述了1INCH的工作原理(此协议的代码比较多,只看了一下主要的流程忽略了具体的交易所实现),在兑换时的步骤具体如下:

  1. 使用getExpectedReturn方法试算本次的最佳兑换方式
  2. 使用getExpectedReturnWithGas方法试算在考虑gas费的情况下的最佳兑换方式
  3. 使用前面的方法返回的值(兑换多少,和兑换渠道)作为参数调用swap方法进行兑换

试算兑换金额和路径的getExpectedReturn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// @notice Calculate expected returning amount of `destToken`
/// @param fromToken (IERC20) Address of token or `address(0)` for Ether
/// @param destToken (IERC20) Address of token or `address(0)` for Ether
/// @param amount (uint256) Amount for `fromToken`
/// @param parts (uint256) Number of pieces source volume could be splitted,
/// works like granularity, higly affects gas usage. Should be called offchain,
/// but could be called onchain if user swaps not his own funds, but this is still considered as not safe.
/// @param flags (uint256) Flags for enabling and disabling some features, default 0
function getExpectedReturn(
IERC20 fromToken, // 从A币
IERC20 destToken, // 兑换成B币
uint256 amount, // 出多少A币
uint256 parts, // 允许拆分成多少份
uint256 flags // See contants in IOneSplit.sol // gas费
)
public
view
returns(
uint256 returnAmount, // 可以兑换多少B币
uint256[] memory distribution // 拆分权重数组,描述的是在哪个交易所兑换多少
)
{
(returnAmount, , distribution) = getExpectedReturnWithGas(
fromToken,
destToken,
amount,
parts,
flags,
0
);
}

交换方法swap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

/// @notice Swap `amount` of first element of `tokens` to the latest element of `destToken`
/// @param tokens (IERC20[]) Addresses of token or `address(0)` for Ether
/// @param amount (uint256) Amount for `fromToken`
/// @param minReturn (uint256) Minimum expected return, else revert
/// @param distribution (uint256[]) Array of weights for volume distribution returned by `getExpectedReturn`
/// @param flags (uint256[]) Flags for enabling and disabling some features, default 0
/// @param referral (address) Address of referral
/// @param feePercent (uint256) Fees percents normalized to 1e18, limited to 0.03e18 (3%)
function swapWithReferralMulti(
IERC20[] memory tokens,
uint256 amount,
uint256 minReturn,
uint256[] memory distribution,
uint256[] memory flags,
address referral,
uint256 feePercent
) public payable returns(uint256 returnAmount) {
require(tokens.length >= 2 && amount > 0, "OneSplit: swap makes no sense");
require(flags.length == tokens.length - 1, "OneSplit: flags array length is invalid");
require((msg.value != 0) == tokens.first().isETH(), "OneSplit: msg.value should be used only for ETH swap");
require(feePercent <= 0.03e18, "OneSplit: feePercent out of range");

uint256 gasStart = gasleft();

Balances memory beforeBalances = _getFirstAndLastBalances(tokens, true);

// Transfer From
if (amount == uint256(-1)) {
amount = Math.min(
tokens.first().balanceOf(msg.sender),
tokens.first().allowance(msg.sender, address(this))
);
}
tokens.first().universalTransferFromSenderToThis(amount);
uint256 confirmed = tokens.first().universalBalanceOf(address(this)).sub(beforeBalances.ofFromToken);

// Swap
tokens.first().universalApprove(address(oneSplitImpl), confirmed);
oneSplitImpl.swapMulti.value(tokens.first().isETH() ? confirmed : 0)(
tokens,
confirmed,
minReturn,
distribution,
flags
);

Balances memory afterBalances = _getFirstAndLastBalances(tokens, false);

// Return
returnAmount = afterBalances.ofDestToken.sub(beforeBalances.ofDestToken);
require(returnAmount >= minReturn, "OneSplit: actual return amount is less than minReturn");
tokens.last().universalTransfer(referral, returnAmount.mul(feePercent).div(1e18));
tokens.last().universalTransfer(msg.sender, returnAmount.sub(returnAmount.mul(feePercent).div(1e18)));

emit Swapped(
tokens.first(),
tokens.last(),
amount,
returnAmount,
minReturn,
distribution,
flags,
referral,
feePercent
);

// Return remainder
if (afterBalances.ofFromToken > beforeBalances.ofFromToken) {
tokens.first().universalTransfer(msg.sender, afterBalances.ofFromToken.sub(beforeBalances.ofFromToken));
}

if ((flags[0] & (FLAG_ENABLE_CHI_BURN | FLAG_ENABLE_CHI_BURN_BY_ORIGIN)) > 0) {
uint256 gasSpent = 21000 + gasStart - gasleft() + 16 * msg.data.length;
_chiBurnOrSell(
((flags[0] & FLAG_ENABLE_CHI_BURN_BY_ORIGIN) > 0) ? tx.origin : msg.sender,
(gasSpent + 14154) / 41947
);
}
else if ((flags[0] & FLAG_ENABLE_REFERRAL_GAS_SPONSORSHIP) > 0) {
uint256 gasSpent = 21000 + gasStart - gasleft() + 16 * msg.data.length;
IReferralGasSponsor(referral).makeGasDiscount(gasSpent, returnAmount, msg.data);
}
}