Skip to content

two tokens (SCD and MCD) support  #318

@akolotov

Description

@akolotov

This issue is continuation of the activity started in #310.

As per the migration plans provided by MakerDAO both SCD and MCD tokens will exist until the Emergency Shutdown (ES) is performed for SCD. It means that during some period of time the price of both tokens will be equal and users will be able to operates with both token contracts. For the xDai bridge this period of time means that users would like to deposit either SCD or MCD to the xDai chain. There is no any risk for such kind of deposits if the bridge will have opportunity to swap new deposited SCD tokens to MCD before SCD ES.

The phase 2 of MCD token support introduction assumes that the xDai bridge functionality will be changed as so any deposit of either SCD or MCD will cause the mint action of the corresponding amount xDai coins in the xDai chain whereas withdrawal of tokens from the xDai chain will lead to unlocking the corresponding amount of MCD tokens.

The overall idea of supporting deposits for both tokens is the following:

  1. The TokenBridge oracle will watch for Transfer event emitted by SCD and MCD token contracts.
  2. If the event was raised by the MCD token contract the deposit confirmed in the same manner as now.
  3. If the event was raised by the SCD token contract, the oracle checks if SCD ES had happened to this moment. No deposit of SCD will be transferred to the xDai chain after ES.
  4. If SCD ES did not take place, the oracle will relay the deposit to the xDai chain.
  5. Then the oracle invokes the method swapTokens from the bridge contract on the Ethereum Mainnet.
  6. The bridge contract will call swapSaiToDai of the MakerDAO migration contract to swap to MCD tokens entire SCD balance owned by the bridge contract.

In case if tokens are being deposited by using the alternative transfer feature with calling relayTokens the tokens swap will be handled by this method rather than by the oracle.

In order to eliminate the possibility of drying out a validator account by deposit requests with small token values, a threshold for a minimum balance of SCD tokens will be introduced. The oracle will not call swapTokens if the current SCD balance of the bridge account is below this threshold. But in order to reduce the time between SCD deposit and its swap to MCD the call of swapTokens will be done every time when validators confirmations are executed for a withdrawal operation.

Bridge contracts modification

Ethereum Mainnet side

In order to support scenarios described above the following changes are needed to be introduced in the TokenBridge contracts:

  • a new method to check the status of SCD ES
  • a new method to choose the validator to call swapTokens for an appeared deposit
  • a new method to store address of the SCD token contract
  • new methods to set and check the minimal SCD balance threshold
  • the new swapTokens method to swap tokens as per request
  • in the relayTokens logic to swap tokens
  • in the executeSignatures logic to swap tokens
  • in the claimTokens logic to forbit claim the SCD tokens till SCD ES.

Checking status of SCD ES

MakerDAO provides a contract top that manages the global settlement. The public integer caged is set up to a time stamp of the block where SCD ES happens.

The address of the contract in the Ethereum Mainnet is 0x9b0ccf7c8994e19f39b2b4cf708e0a7df65fa8a3. The variable caged is zero before ES.

It means that the next method introduced in the bridge contract will return true if the SCD ES has not been applied to the moment of the time stamp provided as the method parameter.

function isTokenSwapAllowed(uint256 _ts) public view returns(bool) {
    uint256 es_ts = ISaiTop(0x9b0ccf7C8994E19F39b2B4CF708e0A7DF65fA8a3).caged();
    if ((es_ts > 0) && (_ts > es_ts)) {
        return false;
    }
    return true;
}

This method is for contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol.

Validator selection

The idea to select the validator to trigger swap for the corresponding deposit of SCD is the following:

  • the validator's node calls the method isValidatorDuty defined below
  • this method uses the latest block number and the number of the validator in the validatators list to determine if this validator is on duty to perform some actions
  • if the node receives true from the method isValidatorDuty it proceeds with the actions to swap SCD tokens
function isValidatorDuty(address _validator) external view returns(bool) {
    uint256 counter = 0;
    address next = getNextValidator(F_ADDR);
    require(next != address(0));

    while (next != F_ADDR) {
        if (next == _validator) {
            return (block.number % validatorCount() == counter);
        }
        
        next = getNextValidator(next);
        counter++;

        require(next != address(0));
    }

    return false;
}

The method should be incorporated into the validators contract.

Since it will be more than one way to swap SCD tokens during the bridge operations it is safe if the validator is not chosen accidentally by this approach. The balance of SCD tokens could be swapped later when another deposit request is handled or during execution of a withdrawal request.

Storing SCD token address

The current version of the bridge contract on the foreign side stores the bridgeable token contract address and provides it through the public method erc20token(). This method is invoked by the oracle, UI and monitor as so these TokenBridge components could interact with the token contract without necessity to point out its address in the corresponding component configuration.

After the Phase 2 migration the ability to get both SCD and MCD tokens contract addresses from the bridge contract is required.

The new method halfduplexErc20token() will returns the address of SCD contract.

function halfduplexErc20token() public pure returns(ERC20Basic) {
    return(ERC20Basic(0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359));
}

This method will be located in contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol.

Minimal SCD balance threshold

Since the new bridge parameter is introduced, new setter and getter methods must be provided in contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol:

function setMinHDTokenBalance(uint256 _minBalance) external onlyOwner {
    uintStorage[MIN_HDTOKEN_BALANCE] = _minBalance;
}

function minHDTokenBalance() external view returns(uint256) {
    return uintStorage[MIN_HDTOKEN_BALANCE];
}

Also it makes sense to provide a method that will check if the SCD token balance is above the threshold as atomic operation:

function isHDTokenBalanceAboveMinBalance() public view returns(bool) {
    if (halfduplexErc20token().balanceOf(address(this)) > uintStorage[MIN_HDTOKEN_BALANCE]) {
        return true;
    }
    return false;
}

Swap tokens operation

The main method that is introduced by the Phase 2 migration allows anyone to swap SCD tokens owned by the bridge contract to MCD. The method will be available until SCD ES is executed. As it was described above the bridge validators are responsible for calling this method as soon as new deposit request of SCD tokens detected.

function swapTokens() public {
    require(isTokenSwapAllowed(now));

    address migrationContract = 0xc73e0383F3Aff3215E6f04B0331D58CeCf0Ab849; //as per https://changelog.makerdao.com/releases/mainnet/1.0.0/contracts.json

    uint256 curHDTokenBalance = halfduplexErc20token().balanceOf(address(this));
    require(curHDTokenBalance > 0);

    uint256 curFDTokenBalance = erc20token().balanceOf(address(this));

    require(halfduplexErc20token().approve(migrationContract, curHDTokenBalance));
    IScdMcdMigration(migrationContract).swapSaiToDai(curHDTokenBalance);

    require(erc20token().balanceOf(address(this)) - curFDTokenBalance == curHDTokenBalance);

    emit TokensSwapped(halfduplexErc20token(), erc20token(), curHDTokenBalance);
}

The migration contract is explicitly specified in the contract since it is assumed that the changes prepared for the Phase 2 are specific for particular pair of tokens.

Interop with alternative receiver feature

Since the scenario to use the alternative receiver feature directly involves the bridge contract to deposit the tokens it allows to swap SCD tokens to MCD in the same transaction.

The current implementation of the feature assumes that only one token contract exists for the bridge operations. Support of two tokens will require extension of existing interfaces as so the contract that deposits tokens by specifying an alternative receiver will need to specify the specific token contract address. But in order to make behavior compatible with other bridge modes four ways to relay tokens will be supported:

  1. relayTokens(address _receiver, uint256 _amount)
  2. relayTokens(address _from, address _receiver, uint256 _amount)
  3. relayTokens(address _receiver, uint256 _amount, address _token)
  4. relayTokens(address _from, address _receiver, uint256 _amount, address _token)

If the token contract address is not specified (the cases 1 and 2) or if it is zero (for the cases 3 and 4), the MCD token contract is used.

Bearing in mind described above, the following changes for the method defined in contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol are suggested:

function _relayTokens(address _sender, address _receiver, uint256 _amount, address _token) internal {
    require(_receiver != bridgeContractOnOtherSide());
    require(_receiver != address(0));
    require(_receiver != address(this));
    require(_amount > 0);
    ERC20Basic tokenToOperate = ERC20Basic(_token);
    ERC20Basic FDToken = erc20token();
    ERC20Basic HDToken = halfduplexErc20token();
    require(tokenToOperate == FDToken || 
            tokenToOperate == ERC20Basic(0x0) || 
            tokenToOperate == HDToken);
            
    require(withinLimit(_amount));
    setTotalSpentPerDay(getCurrentDay(), totalSpentPerDay(getCurrentDay()).add(_amount));

    if (tokenToOperate == ERC20Basic(0x0)) {
        tokenToOperate = FDToken;
    }
    
    tokenToOperate.transferFrom(_sender, address(this), _amount);
    emit UserRequestForAffirmation(_receiver, _amount);
    if (tokenToOperate == HDToken) {
        swapTokens();
    }
}

Implicit tokens swap

The current bridge architecture allows to implement the implicit token swap operation which will handle non-zero balance of the SCD tokens. This balance could appear if a SCD deposit occurred with value less or equal the minimal balance threshold. Another reason of non-zero balance is the case when the bridge validator skips his duty to call the swapTokens() method.

The implicit token swap is integrated in the flow when the method executeSignatures is being executed:

function onExecuteMessage(
    address _recipient,
    uint256 _amount,
    bytes32 /*_txHash*/
) internal returns (bool) {
    setTotalExecutedPerDay(getCurrentDay(), totalExecutedPerDay(getCurrentDay()).add(_amount));
    uint256 amount = _amount.div(10**decimalShift());
    uint256 amount = _amount;
    bool res = erc20token().transfer(_recipient, amount);
    
    if (halfduplexErc20token().balanceOf(address(this)) > 0) {
        address(this).call(abi.encodeWithSelector(bytes4(keccak256(bytes("swapTokens()")))));
    }

    return res;
}

Limited ability to claim SCD tokens

The TokenBridge contracts provides ability for the bridge owners to claim tokens (native or ERC20) owned by the bridge contract if they were occasionally transferred to the contract. Any token except the bridgeable one could be claimed and sent to the initial originator.

As per considered design the SCD token is bridgeable until SCD ES is executed. So, the ability to claim SCD tokens must be restricted till the SCD ES execution as well.

For this, the method claimTokens of contracts/upgradeable_contracts/erc20_to_native/ForeignBridgeErcToNative.sol should be extended like this:

function claimTokens(address _token, address _to) public {
    require(_token != address(erc20token()));
    if (_token == address(halfduplexErc20token())) {
        // SCD is not claimable if the bridge accepts deposits of this token
        require(!isTokenSwapAllowed(now));
    }
    super.claimTokens(_token, _to);
}

xDai chain side

Since the only bridge operation that is possible with the SCD token is the deposit no changes are needed on the xDai chain side.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions