Bancor协议源码分析

1.编译源码、运行测试脚本

  • pull Bancor源码到当前目录。
  • 在当前目录执行npm install 安装依赖的包
  • 进入scripts目录,修改三个js文件中前面几行代码中的相对路径,如“./solidity”改为“…/solidity”。否则会提示“Error: spawn node ENOENT”错误。
  • 在scripts目录执行node rebuild-all.js,编译源代码
  • 在scripts目录执行node run-test.js,运行测试脚本
    Bancor协议源码分析_第1张图片
    等待一段时间后,便能够看到类似上图的测试结果

2.代码结构分析

抛开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进行了测试,十分细致。

3.业务流程分析

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 。

你可能感兴趣的:(BlockChain,Solidity)