enzymefinance / enzip

Enzyme Improvement Proposal
https://github.com/enzymefinance/ENZIP
8 stars 11 forks source link

MIP4 - Autonomous Melon Funds with Chronos #4

Closed e00dan closed 1 year ago

e00dan commented 6 years ago

Autonomous Melon Funds with Chronos [Specification]

Authors: Logan Saether <lsaether@protonmail.com>, Daniel Kmak <daniel@chronologic.network> Status: Dependent on MIP3

Summary

Follows is the specification for an integration of the Chronos conditional scheduler with the Melon Fund to allow for on-chain autonomous operations such as automatic stop-loss order making, or take-profit order making, that only needs to be set once by the owner and will be trustlessly executed when a specific condition is met such as the change of a price feed. So far we estimate that the integration will be compatible with the ZeroEx v2.0.0 exchange contracts, and any other exchanges which implement a similar Signature Validation scheme.

Flow

Overview

 - Deploy a Proxy Wallet
 - Deploy Melon Fund through Proxy Wallet
 - Schedule transaction on Proxy Wallet
 - Use ScheduledTransaction address 
 - Success [Automation]

The creator of an autonomous Melon Fund will first deploy a Proxy Wallet, a wallet contract that has an integration with the Chronos interface and allows the scheduling of transactions to pass through the wallet. The creator will then deploy a new Melon Fund by sending a proxy transaction through the Proxy Wallet, thereby making the Proxy Wallet the owner of the new Melon Fund. Any function on the Melon Fund which checks the onlyOwner modifier will have to be sent through the Proxy Wallet.

Now if the creator would like to set on-chain, trustless order making which will create a new order when a condition is met (the price of an asset reaches some threshold), they will prepare the data for the transaction, then send it to the Proxy Wallet's schedule function which will deploy a new ScheduledTransaction. The ScheduledTransaction will be executed by the decentralized network of Chronos TimeNodes when the conditions return true and the block.number or block.timestamp is within the executionWindow. The ScheduledTransaction address will be whitelisted with the Proxy Wallet so that when the transaction is executed it is allowed to send data through the proxy function on the Proxy Wallet and into the Melon Fund with the context of being owner of the Melon Fund.

Proxy Wallet

A sample implementation of the proxy wallet may look like this:

contract ProxyWallet {
    mapping(address => bool) whitelist;
    Scheduler scheduler;

    function ProxyWallet(address _chronosScheduler) {
        whitelist[msg.sender] = true;
        scheduler = Scheduler(_chronosScheduler);
    }

    modifier isWhitelisted(address _test) {
        require(whitelist[_test]);
        _;
    }

    function proxy(
        address _target,
        bytes _callData
    ) isWhitelisted(msg.sender) payable {
        _target.call.value(msg.value)(_callData);
    }

    function schedule(bytes _serializedTransaction)
        payable
        isWhitelisted(msg.sender)
    {
        address scheduledTransaction = scheduler.schedule.value(msg.value)(_serializedTransaction);
        //sanity
        require(scheduledTransaction != 0x0);
        whitelist[scheduledTransaction] = true;
    }
}

This allows for any transaction scheduled by a whitelisted account to itself be whitelisted and therefore have owner priviledges on this contract.

Off-chain scheduled order making

In ZeroEx v2 the order book is off-chain. This implies, that for setting up a stop-loss (make order when price < X), three things need to be done on-chain:

  1. Fund tokens should be approved, so ZeroEx exchange can use it to settle order
  2. Transaction needs to be scheduled with execution window and conditional check set up
  3. Chronos ZeroEx Validator needs to be approved to validate signatures

After these steps, fund owner can make order on the ZeroEx v2 exchange, by signing order hash using Chronos specific signature. Then signed order needs to be published to the ZeroEx relayers, which maintain the order book.

The order will be published in the order book only when signature will be valid. The signature of order will only be valid when scheduled transaction is in execution window and the condition is met (ex. price < X).

Prepared Data (SerializedTransaction)

To schedule with the Chronos scheduler requires the data of the transaction beforehand. Usually with decentralized exchanges they require the signature of the order, which would cause a problem since it would make the order able to be executed immediately even if we wanted it executed later. However, as we show later the new SignatureValidator scheme implemented in v2.0.0 of ZeroEx protocol opens the door for use to prepare the data in a certain way to allow us to send the entire transaction with signature before hand.

ZeroEx v2 SignatureValidator

Validator contract approval by signer

Before using validator contract, signer has to approve validator contract address. This can be done by calling:

MixinSignatureValidator.setSignatureValidatorApproval(
    address validatorAddress,
    bool approval
)

with arguments:

ChronosValidator

ChronosValidator implements IValidator. It needs to be deployed once per blockchain.

When validator contract is approved, we call:

MixinSignatureValidator.isValidSignature(
    bytes32 hash,
    address signerAddress,
    bytes memory signature
)

Last byte of signature needs to be: 0x06 - SignatureType.Validator. The structure of signature of such type is:

// A signature using this type should be encoded as:
// | Offset   | Length | Contents                        |
// | 0x00     | x      | Signature to validate           |
// | 0x00 + x | 20     | Address of validator contract   |
// | 0x14 + x | 1      | Signature type is always "\x06" |

This will send internal transaction call to the address of validator contract:

IValidator(validatorAddress).isValidSignature(
    hash,
    signerAddress,
    signature
);

Validator contract needs to implement IValidator interface and specifically method isValidSignature:

contract IValidator {

    /// @dev Verifies that a signature is valid.
    /// @param hash Message hash that is signed.
    /// @param signerAddress Address that should have signed the given hash.
    /// @param signature Proof of signing.
    /// @return Validity of order signature.
    function isValidSignature(
        bytes32 hash,
        address signerAddress,
        bytes signature
    )
        external
        view
        returns (bool isValid);
}

In case of making order the call parameter should be as below:

bytes32 hash = orderHash (ZeroEx order method arguments)
address signerAddress = orderMakerAddress
bytes signature = CHRONOS_SPECIFIC_SIGNATURE

Where CHRONOS_SPECIFIC_SIGNATURE scheme is:

Offset Length (bytes) Contents
0x00 20 Scheduled tx address
0x14 32 Length of next information (x)
0x34 x Chronos serialized scheduled tx data
0x34 + x y Signed scheduled tx address signature

Example implementation of isValidSignature method by Chronos Validator:

contract ChronosValidator is IValidator {
    function decodeSignature(
        bytes signature
    ) public pure returns (
        address scheduledTxAddress,
        bytes memory serializedTransaction,
        bytes memory signed
    ) {
        scheduledTxAddress = LibBytes.readAddress(signature, 0x00);

        uint256 serializedTransactionLength = LibBytes.readUint256(signature, 0x14);

        serializedTransaction = LibBytes.slice(signature, 0x34, 0x34 + serializedTransactionLength);

        signed = LibBytes.slice(signature, 0x34 + serializedTransactionLength, signature.length);
    }

    function recoverAddress(
        bytes signature,
        address scheduledTxAddress,
        bytes32 orderHash
    ) public pure returns (address recovered) {
        uint8 v = uint8(signature[0]);
        bytes32 r = LibBytes.readBytes32(signature, 1);
        bytes32 s = LibBytes.readBytes32(signature, 33);

        recovered = ecrecover(
            keccak256(
                abi.encodePacked(
                    "\x19Ethereum Signed Message:\n52",
                    scheduledTxAddress,
                    orderHash
                )
            ),
            v,
            r,
            s
        );
    }

    /// @dev Verifies that a signature is valid.
    /// @param hash Message hash that is signed.
    /// @param signerAddress Address that should have signed the given hash.
    /// @param signature Proof of signing.
    /// @return Validity of order signature.
    function isValidSignature(
        bytes32 hash,
        address signerAddress,
        bytes signature
    )
        external
        view
        returns (
            bool isValid
        )
    {
        (address scheduledTxAddress, bytes memory serializedTransaction, bytes memory signed) = decodeSignature(signature);

        address recovered = recoverAddress(signed, scheduledTxAddress, hash);

        IScheduledTransaction scheduledTx = IScheduledTransaction(scheduledTxAddress);

        address scheduledTxOwner = Ownable(scheduledTx.owner()).owner();
        isValid = (scheduledTxOwner == recovered) && (scheduledTxOwner == signerAddress);

        if (isValid) {
            isValid = scheduledTx.canExecute(serializedTransaction);
        }
    }
}
Repository with tests

The repository with Solidity code and tests implementing ChronosValidator: https://github.com/chronologic/chronos-melonport.

References