抛开Bancor的模式创新,单从技术上来讲,他已经是以太坊上规模比较大的项目了:将近四十个合约(包括基类合约),最终部署的合约也多达18个合约。
先看Bancor项目的目录结构:
|-- scripts
|-- fix-modules.js
|-- rebuild-all.js
|-- run-tests.js
|-- solidity
|-- contracts
|-- migrations
|-- python
|-- test
|-- truffle.js
|-- truffle-config.js
scripts目录下放的是辅助编译、测试的js脚本文件,可以用这里的文件实现一键测试
solidity目录是一个标准的truffle工程目录。其中test目录里针对每个sol都有一个与之对应的js测试文件,文件里的测试用例非常详细,例如,针对BancorConverter合约的构造函数的测试用例:
合约的构造函数
constructor(
ISmartToken _token,
IContractRegistry _registry,
uint32 _maxConversionFee,
IERC20Token _connectorToken,
uint32 _connectorWeight
)
public
SmartTokenController(_token)
validAddress(_registry)
validMaxConversionFee(_maxConversionFee)
{
registry = _registry;
prevRegistry = _registry;
IContractFeatures features = IContractFeatures(registry.addressOf(ContractIds.CONTRACT_FEATURES));
// initialize supported features
if (features != address(0))
features.enableFeatures(FeatureIds.CONVERTER_CONVERSION_WHITELIST, true);
maxConversionFee = _maxConversionFee;
if (_connectorToken != address(0))
addConnector(_connectorToken, _connectorWeight, false);
}
针对构造函数的测试用例
it('verifies the converter data after construction', async () => {
let converter = await BancorConverter.new(tokenAddress, contractRegistry.address, 0, '0x0', 0);
let token = await converter.token.call();
assert.equal(token, tokenAddress);
let registry = await converter.registry.call();
assert.equal(registry, contractRegistry.address);
let featureWhitelist = await converter.CONVERTER_CONVERSION_WHITELIST.call();
let isSupported = await contractFeatures.isSupported.call(converter.address, featureWhitelist);
assert(isSupported);
let maxConversionFee = await converter.maxConversionFee.call();
assert.equal(maxConversionFee, 0);
let conversionsEnabled = await converter.conversionsEnabled.call();
assert.equal(conversionsEnabled, true);
});
it('should throw when attempting to claim tokens when not enabled', async () => {
let converter = await initConverter(accounts, true);
try {
await converter.claimTokens(accounts[0], 1);
assert(false, "didn't throw");
} catch(error) {
return utils.ensureException(error);
}
});
it('should throw when attempting to construct a converter with no token', async () => {
try {
await BancorConverter.new('0x0', contractRegistry.address, 0, '0x0', 0);
assert(false, "didn't throw");
}
catch (error) {
return utils.ensureException(error);
}
});
it('should throw when attempting to construct a converter with no contract registry', async () => {
try {
await BancorConverter.new(tokenAddress, '0x0', 0, '0x0', 0);
assert(false, "didn't throw");
}
catch (error) {
return utils.ensureException(error);
}
});
it('should throw when attempting to construct a converter with invalid max fee', async () => {
try {
await BancorConverter.new(tokenAddress, contractRegistry.address, 1000000000, '0x0', 0);
assert(false, "didn't throw");
}
catch (error) {
return utils.ensureException(error);
}
});
it('verifies the first connector when provided at construction time', async () => {
let converter = await BancorConverter.new(tokenAddress, contractRegistry.address, 0, connectorToken.address, 200000);
let connectorTokenAddress = await converter.connectorTokens.call(0);
assert.equal(connectorTokenAddress, connectorToken.address);
let connector = await converter.connectors.call(connectorTokenAddress);
verifyConnector(connector, true, true, 200000, false, 0);
});
每个测试用例都通过new一个新的实例来测试指定的功能,该方法只适合虚拟机环境下。其中多个用例还对revert进行了测试,十分细致。
bancor相当于一个去中心化的做市商,用于解决小币种的流动性问题。具体的可参见白皮书。这里从代码角度分析Bancor的业务流程。
最重要的合约便是BancorConverter合约,它首先是一个SmartTokenController,即:该合约可以控制一个smartToken的issue与destroy,另外,它还有一个connectors数组,里面会有一个或多个connectorToken,每个connectorToken在添加到BancorConverter中之后,都需要将一定量的connectorToken转给BancorConverter作为准备金,准备金的数量以及connectorToken的权重直接决定了connectorToken与其他token的兑换比例。
通过测试代码看一下BancorConverter的初始化:
async function initConverter(accounts, activate, maxConversionFee = 0) {
token = await SmartToken.new('Token1', 'TKN1', 2);
tokenAddress = token.address;
let converter = await BancorConverter.new(
tokenAddress,
contractRegistry.address,
maxConversionFee,
connectorToken.address,
250000
);
let converterAddress = converter.address;
await converter.addConnector(connectorToken2.address, 150000, false);
await token.issue(accounts[0], 20000);
转准备金
await connectorToken.transfer(converterAddress, 5000);
await connectorToken2.transfer(converterAddress, 8000);
if (activate) {
将smartToken的控制权完全交给converter
await token.transferOwnership(converterAddress);
await converter.acceptTokenOwnership();
}
return converter;
}
上述代码创建了一个拥有2个connectorToken的converter,于是,该converter就可以实现这三种代币之间的转换:
在有多个converter的情况下,用户不知道这写converter的地址,于是可以通过指定一个兑换路径(path),使用统一入口:BancorNetwork来进行兑换。
function convertByPath(
IERC20Token[] _path,
uint256 _amount,
uint256 _minReturn,
IERC20Token _fromToken,
address _for
) private returns (IERC20Token, uint256) {
ISmartToken smartToken;
IERC20Token toToken;
IBancorConverter converter;
// get the contract features address from the registry
IContractFeatures features = IContractFeatures(registry.addressOf(ContractIds.CONTRACT_FEATURES));
// iterate over the conversion path
uint256 pathLength = _path.length;
for (uint256 i = 1; i < pathLength; i += 2) {
smartToken = ISmartToken(_path[i]);
toToken = _path[i + 1];
converter = IBancorConverter(smartToken.owner());
checkWhitelist(converter, _for, features);
// if the smart token isn't the source (from token), the converter doesn't have control over it and thus we need to approve the request
if (smartToken != _fromToken)
ensureAllowance(_fromToken, converter, _amount);
// make the conversion - if it's the last one, also provide the minimum return value
_amount = converter.change(_fromToken, toToken, _amount, i == pathLength - 2 ? _minReturn : 1);
_fromToken = toToken;
}
return (toToken, _amount);
}
path是一个地址数组,举例说明:
tokenA <=> smartTokenX <=> tokenB (converterA)
smartTokenX <=> smartTokenY <=> tokenC (converterB)
想要将手中的tokenA兑换成tokenC, path需要这样填写
[tokenA.address, smartTokenX.address, smartTokenX.address, smartTokenY.address, tokenC.address]
- 在convertByPath中会先通过smartTokenX.owner查到converterA,
- 通过converterA将手中的tokenA兑换为smartTokenA,
- 然后通过smartTokenY.owner 查到converterB,
- 通过converterB将smartTokenA兑换为tokenC 。