RigoBlock / 0x-manager

0 stars 0 forks source link

Custom exchange contract. #1

Open wnz99 opened 6 years ago

wnz99 commented 6 years ago

Vanilla 0x

Order format

Users that create an order are called Makers and they need to specify some information in their order so the exchange.sol smart contract knows what to do with them.

// Generate order
const order = {
    maker: makerAddress,
    taker: ZeroEx.NULL_ADDRESS,
    feeRecipient: ZeroEx.NULL_ADDRESS,
    makerTokenAddress: ZRX_ADDRESS,
    takerTokenAddress: WETH_ADDRESS,
    exchangeContractAddress: EXCHANGE_ADDRESS,
    salt: ZeroEx.generatePseudoRandomSalt(),
    makerFee: new BigNumber(0),
    takerFee: new BigNumber(0),
    makerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(0.2), DECIMALS), // Base 18 decimals
    takerTokenAmount: ZeroEx.toBaseUnitAmount(new BigNumber(0.3), DECIMALS), // Base 18 decimals
    expirationUnixTimestampSec: new BigNumber(Date.now() + 3600000), // Valid for up to an hour
};

where the fields are:

maker : Ethereum address of our Maker.
taker : Ethereum address of our Taker.
feeRecipient : Ethereum address of our Relayer (none for now).
makerTokenAddress: The token address the Maker is offering.
takerTokenAddress: The token address the Maker is requesting from the Taker.
exchangeContractAddress : The exchange.sol address.
salt: Random number to make the order (and therefore its hash) unique.
makerFee: How many ZRX the Maker will pay as a fee to the Relayer.
takerFee : How many ZRX the Taker will pay as a fee to the Relayer.
makerTokenAmount: The amount of token the Maker is offering.
takerTokenAmount: The amount of token the Maker is requesting from the Taker.
expirationUnixTimestampSec: When will the order expire (in unix time)

The NULL_ADDRESS is for the taker field since in our case we do not care who the taker will be and using NULL_ADDRESS will allow anyone to fill our order.

The orders must comply to the following schema. Otherwise an relay might reject the order:

https://github.com/0xProject/0x-monorepo/blob/d4c1b3b0bd26e730ce6687469cdf7283877543e1/packages/json-schemas/schemas/order_schemas.ts#L1

export const orderSchema = {
    id: '/Order',
    properties: {
        maker: { $ref: '/Address' },
        taker: { $ref: '/Address' },
        makerFee: { $ref: '/Number' },
        takerFee: { $ref: '/Number' },
        makerTokenAmount: { $ref: '/Number' },
        takerTokenAmount: { $ref: '/Number' },
        makerTokenAddress: { $ref: '/Address' },
        takerTokenAddress: { $ref: '/Address' },
        salt: { $ref: '/Number' },
        feeRecipient: { $ref: '/Address' },
        expirationUnixTimestampSec: { $ref: '/Number' },
        exchangeContractAddress: { $ref: '/Address' },
    },
    required: [
        'maker',
        'taker',
        'makerFee',
        'takerFee',
        'makerTokenAmount',
        'takerTokenAmount',
        'salt',
        'feeRecipient',
        'expirationUnixTimestampSec',
        'exchangeContractAddress',
    ],
    type: 'object',
};

export const signedOrderSchema = {
    id: '/SignedOrder',
    allOf: [
        { $ref: '/Order' },
        {
            properties: {
                ecSignature: { $ref: '/ECSignature' },
            },
            required: ['ecSignature'],
        },
    ],
};

The schema validation is done with the following package:

https://github.com/tdegrunt/jsonschema

We need to check if additional parameters in an order will make the validation fail. At a first look it seems that validation only check the specific variables are included and that they have a specific type. Therefore, it seems that we can add additional variable to an order and it will not fail validation on third parties relays.

Signing the order

Now that we created an order as a Maker, we need to prove that we actually own the address specified as makerAddress. After all, we could always try pretending to be someone else just to annoy an exchange and other traders! To do so, we will sign the orders with the corresponding private key and append the signature to our order.

You can first obtain the order hash with the following command:

const orderHash = ZeroEx.getOrderHashHex(order);

We will need to modify the above function in oder to include any additional fields, or we can just create our own hashing function.

    getOrderHashHex(order: Order | SignedOrder): string {
        const orderParts = [
            { value: order.exchangeContractAddress, type: SolidityTypes.Address },
            { value: order.maker, type: SolidityTypes.Address },
            { value: order.taker, type: SolidityTypes.Address },
            { value: order.makerTokenAddress, type: SolidityTypes.Address },
            { value: order.takerTokenAddress, type: SolidityTypes.Address },
            { value: order.feeRecipient, type: SolidityTypes.Address },
            {
                value: utils.bigNumberToBN(order.makerTokenAmount),
                type: SolidityTypes.Uint256,
            },
            {
                value: utils.bigNumberToBN(order.takerTokenAmount),
                type: SolidityTypes.Uint256,
            },
            {
                value: utils.bigNumberToBN(order.makerFee),
                type: SolidityTypes.Uint256,
            },
            {
                value: utils.bigNumberToBN(order.takerFee),
                type: SolidityTypes.Uint256,
            },
            {
                value: utils.bigNumberToBN(order.expirationUnixTimestampSec),
                type: SolidityTypes.Uint256,
            },
            { value: utils.bigNumberToBN(order.salt), type: SolidityTypes.Uint256 },
        ];
        const types = _.map(orderParts, o => o.type);
        const values = _.map(orderParts, o => o.value);
        const hashBuff = ethABI.soliditySHA3(types, values);
        const hashHex = ethUtil.bufferToHex(hashBuff);
        return hashHex;
    }

https://github.com/0xProject/0x-monorepo/blob/d4c1b3b0bd26e730ce6687469cdf7283877543e1/packages/0x.js/src/utils/utils.ts#L21

Now that we have the order hash, we can sign it and append the signature to the order;

// Signing orderHash -> ecSignature
const shouldAddPersonalMessagePrefix = false;
const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress, shouldAddPersonalMessagePrefix);

// Append signature to order
const signedOrder = {
    ...order,
    ecSignature,
};

With this, anyone can verify that the signature is authentic and this will prevent any change to the order by a third party. If the order is changed by even a single bit, then the hash of the order will be different and therefore invalid when compared to the signed hash.

The code which signs the order is the following:

    public async signOrderHashAsync(
        orderHash: string,
        signerAddress: string,
        shouldAddPersonalMessagePrefix: boolean,
    ): Promise<ECSignature> {
        assert.isHexString('orderHash', orderHash);
        await assert.isSenderAddressAsync('signerAddress', signerAddress, this._web3Wrapper);
        const normalizedSignerAddress = signerAddress.toLowerCase();

        let msgHashHex = orderHash;
        if (shouldAddPersonalMessagePrefix) {
            const orderHashBuff = ethUtil.toBuffer(orderHash);
            const msgHashBuff = ethUtil.hashPersonalMessage(orderHashBuff);
            msgHashHex = ethUtil.bufferToHex(msgHashBuff);
        }

        const signature = await this._web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex);

        // HACK: There is no consensus on whether the signatureHex string should be formatted as
        // v + r + s OR r + s + v, and different clients (even different versions of the same client)
        // return the signature params in different orders. In order to support all client implementations,
        // we parse the signature in both ways, and evaluate if either one is a valid signature.
        const validVParamValues = [27, 28];
        const ecSignatureVRS = signatureUtils.parseSignatureHexAsVRS(signature);
        if (_.includes(validVParamValues, ecSignatureVRS.v)) {
            const isValidVRSSignature = ZeroEx.isValidSignature(orderHash, ecSignatureVRS, normalizedSignerAddress);
            if (isValidVRSSignature) {
                return ecSignatureVRS;
            }
        }

        const ecSignatureRSV = signatureUtils.parseSignatureHexAsRSV(signature);
        if (_.includes(validVParamValues, ecSignatureRSV.v)) {
            const isValidRSVSignature = ZeroEx.isValidSignature(orderHash, ecSignatureRSV, normalizedSignerAddress);
            if (isValidRSVSignature) {
                return ecSignatureRSV;
            }
        }

        throw new Error(ZeroExError.InvalidSignature);
    }

Now let's actually verify whether the order we created is valid

await zeroEx.exchange.validateOrderFillableOrThrowAsync(signedOrder);

If something was wrong with our order, this function would throw an informative error. If it passes, then the order is currently fillable.

Filling the order

We are skipping setting the allowance but this needs to be done before filling the order.

Now let's try to fill the order:

const txHash = await zeroEx.exchange.fillOrderAsync(
    signedOrder,
    fillTakerTokenAmount,
    shouldThrowOnInsufficientBalanceOrAllowance,
    takerAddress,
);

RigoBlock version

Signed order creation

As we saw earlier we can add additional fields to an signed order, for example we could a variable such fundMaker:

fundMaker : Ethereum address of our Fund.
maker : Ethereum address of our Maker.
taker : Ethereum address of our Taker.
feeRecipient : Ethereum address of our Relayer (none for now).
makerTokenAddress: The token address the Maker is offering.
takerTokenAddress: The token address the Maker is requesting from the Taker.
exchangeContractAddress : The exchange.sol address.
salt: Random number to make the order (and therefore its hash) unique.
makerFee: How many ZRX the Maker will pay as a fee to the Relayer.
takerFee : How many ZRX the Taker will pay as a fee to the Relayer.
makerTokenAmount: The amount of token the Maker is offering.
takerTokenAmount: The amount of token the Maker is requesting from the Taker.
expirationUnixTimestampSec: When will the order expire (in unix time)

In the exchangeContractAddress we would enter our custom Exchange contract address.

Then, the manager would sign the order and our platform sends it to our relay or any other. Validation should not fail.

The fund will approve an allowance on the token contract.

Exchange smart contract

On the contract, at the following function:

https://github.com/0xProject/0x-monorepo/blob/d263f7783fabe89cc9714b596068eccdc5babc1c/packages/deployer/test/fixtures/contracts/Exchange.sol#L107

1) We would get the fund address from the maker address and put into a variable such dragoAddress 2) We would change the code from:

```solidity
Order memory order = Order({
    maker: orderAddresses[0],
    taker: orderAddresses[1],
    makerToken: orderAddresses[2],
    takerToken: orderAddresses[3],
    feeRecipient: orderAddresses[4],
    makerTokenAmount: orderValues[0],
    takerTokenAmount: orderValues[1],
    makerFee: orderValues[2],
    takerFee: orderValues[3],
    expirationTimestampInSec: orderValues[4],
    orderHash: getOrderHash(orderAddresses, orderValues)
});
```
to

```solidity
Order memory order = Order({
    maker: orderAddresses[0],
    taker: orderAddresses[1],
    makerToken: orderAddresses[2],
    takerToken: orderAddresses[3],
    feeRecipient: orderAddresses[4],
    makerTokenAmount: orderValues[0],
    takerTokenAmount: orderValues[1],
    makerFee: orderValues[2],
    takerFee: orderValues[3],
    expirationTimestampInSec: orderValues[4],
    orderHash: getOrderHash(orderAddresses, orderValues, dragoAddress)
});
```

3) We would change the hashing funtion in order to give the same output as the js amended funtion above:

        function getOrderHash(address[5] orderAddresses, uint[6] orderValues)
            public
            constant
            returns (bytes32)
        {
            return keccak256(
                address(this),
                orderAddresses[0], // maker
                orderAddresses[1], // taker
                orderAddresses[2], // makerToken
                orderAddresses[3], // takerToken
                orderAddresses[4], // feeRecipient
                orderValues[0],    // makerTokenAmount
                orderValues[1],    // takerTokenAmount
                orderValues[2],    // makerFee
                orderValues[3],    // takerFee
                orderValues[4],    // expirationTimestampInSec
                orderValues[5]     // salt
            );
        }

We need to put the fund address some where in there.

The signature should validate correctly at the following line:

require(isValidSignature(
    order.maker,
    order.orderHash,
    v,
    r,
    s
));

Finally, we will need to enter the correct parameters in the following transferViaTokenTransferProxy so that the tokens are transferred from the fund address to the taker.

Will it work? God will tell us...

wnz99 commented 6 years ago

So, basically, we need that the following Javascript and Solidity codes return the same hash if we add ad additional field for the fund address.

With regards to the Javascript, we can easily implement our own hashing function, based on the Solidity one.

If they return the same hash, then the validation of the signature will succeed and the order will be filled in the exchange contract.

The only additional amendment to the contract would be making sure that the tokens are transferre from the fund address rather than the manager's inside the token contract, as explained at the end of my previous post.

Javascript:

    getOrderHashHex(order: Order | SignedOrder): string {
        const orderParts = [
            { value: order.exchangeContractAddress, type: SolidityTypes.Address },
            { value: order.maker, type: SolidityTypes.Address },
            { value: order.taker, type: SolidityTypes.Address },
            { value: order.makerTokenAddress, type: SolidityTypes.Address },
            { value: order.takerTokenAddress, type: SolidityTypes.Address },
            { value: order.feeRecipient, type: SolidityTypes.Address },
            {
                value: utils.bigNumberToBN(order.makerTokenAmount),
                type: SolidityTypes.Uint256,
            },
            {
                value: utils.bigNumberToBN(order.takerTokenAmount),
                type: SolidityTypes.Uint256,
            },
            {
                value: utils.bigNumberToBN(order.makerFee),
                type: SolidityTypes.Uint256,
            },
            {
                value: utils.bigNumberToBN(order.takerFee),
                type: SolidityTypes.Uint256,
            },
            {
                value: utils.bigNumberToBN(order.expirationUnixTimestampSec),
                type: SolidityTypes.Uint256,
            },
            { value: utils.bigNumberToBN(order.salt), type: SolidityTypes.Uint256 },
        ];
        const types = _.map(orderParts, o => o.type);
        const values = _.map(orderParts, o => o.value);
        const hashBuff = ethABI.soliditySHA3(types, values);
        const hashHex = ethUtil.bufferToHex(hashBuff);
        return hashHex;
    }

Solidity:

    /// @dev Calculates Keccak-256 hash of order with specified parameters.
    /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient.
    /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt.
    /// @return Keccak-256 hash of order.
    function getOrderHash(address[5] orderAddresses, uint[6] orderValues)
        public
        constant
        returns (bytes32)
    {
        return keccak256(
            address(this),
            orderAddresses[0], // maker
            orderAddresses[1], // taker
            orderAddresses[2], // makerToken
            orderAddresses[3], // takerToken
            orderAddresses[4], // feeRecipient
            orderValues[0],    // makerTokenAmount
            orderValues[1],    // takerTokenAmount
            orderValues[2],    // makerFee
            orderValues[3],    // takerFee
            orderValues[4],    // expirationTimestampInSec
            orderValues[5]     // salt
        );
    }
wnz99 commented 6 years ago

Proof of concept:

    /// @dev Fills the input order.
    /// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient.
    /// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt.
    /// @param fillTakerTokenAmount Desired amount of takerToken to fill.
    /// @param shouldThrowOnInsufficientBalanceOrAllowance Test if transfer will fail before attempting.
    /// @param v ECDSA signature parameter v.
    /// @param r ECDSA signature parameters r.
    /// @param s ECDSA signature parameters s.
    /// @return Total amount of takerToken filled in trade.

    // Aggiungiamo un elemento all'input array address per passare l'indirizzo del drago, che quindi diventa:
    //
    // address[0], // maker
    // address[1], // taker
    // address[2], // makerToken
    // address[3], // takerToken
    // address[4], // feeRecipient
    // address[5], // fundAddress  <== INDIRIZZO DRAGO

    function fillOrder(
          address[] orderAddresses, // <== RENDIAMO QUESTO INPUT DINAMICO, OPPURE LO DEFINIAMO COME address[6]. DA VALUTARE.
          uint[6] orderValues,
          uint fillTakerTokenAmount,
          bool shouldThrowOnInsufficientBalanceOrAllowance,
          uint8 v,
          bytes32 r,
          bytes32 s)
          public
          returns (uint filledTakerTokenAmount)
    {
        // A SECONDA CHE SI TRATTI DI UN ORDINE MAKER DI UN DRAGO O DI UN ALTRO MAKER, DOBBIAMO DEFINIRE IL MODO IN CUI
        // VIENE CALCOLATO L'HASH DELL'ORDINE. QUESTO ROMPE COMPATILITA' CON IL MODO IN CUI NOI NELLA NOSTRA PIATTAFORMA CALCOLIAMO
        // L'HASH DEGLI ORDINI. DOBBIAMO FARE UNA FUNZIONE NOSTRA CUSTOM.
        // NOI INSERIAMO L'INDIRIZZO DEL DRAGO NELLA FIRMA DELL'ORDINE MAKER PERCHE' IN QUESTO MODO UN ORDINE SIGLATO PER CONTO DEL DRAGO
        // NON PASSA LA VALIDAZIONE SUL UN EXCHANGE 0X VANILLA. PERO' QUESTO ANCHE NON FA PASSARE LA VALIDAZIONE SUI RELAY QUANDO INVIAMO UN ORDINE
        // A MENO CHE IL RELAY NON SAPPIA COME VERIFICARLO CORRETTAMENTE. QUESTO E' UN PROBLEMA.
        bytes32 orderHash;
        address tokenTransferProxyMaker; // <== QUESTO CI SERVER ALLA FINE PER TRASFERIRE CORRETTAMENTE IL TOKEON DAL FONDO
        if (orderAddresses[5] == address(0)) {
            tokenTransferProxyMaker = orderAddresses[0];
            orderHash = getOrderHash(orderAddresses, orderValues); // <== HO FATTO UNA MODIFICA A QUESTA FUNZIONE. VERIFICA SE' E' CORRETTO. HO MODIFICATO L'INPUT orderAddess  DA STATIC A DINAMICO
        }
        if (orderAddresses[5] != address(0)) {
            address ownerAddress = getDragoOwner(order.maker); // <==  FACCIAMO UNA VERIFICA SULL'OWNER. SE NON PASSA L'ORDINE NON VIENE ESEGUITO.
            if (ownerAddress != orderAddresses[0]) {
                return 0;
            }
            tokenTransferProxyMaker = orderAddresses[5];
            orderHash = getOrderHashFund(orderAddresses, orderValues); // <== HO AGGIUNTO QUESTA FUNZIONE. SE C'E' UN MODO EFFICIENTE DI FARE TUTTO CON UNA SOLA FUNZIONE, VEDI TU.
        }
        Order memory order = Order({
            maker: orderAddresses[0],
            taker: orderAddresses[1],
            makerToken: orderAddresses[2],
            takerToken: orderAddresses[3],
            feeRecipient: orderAddresses[4],
            makerTokenAmount: orderValues[0],
            takerTokenAmount: orderValues[1],
            makerFee: orderValues[2],
            takerFee: orderValues[3],
            expirationTimestampInSec: orderValues[4],
            orderHash: orderHash // <== L'HASH CHE ABBIAMO CALCOLATO PRECEDENTEMENTE
        });
        require(order.taker == address(0) || order.taker == msg.sender);
        require(order.makerTokenAmount > 0 && order.takerTokenAmount > 0 && fillTakerTokenAmount > 0);
        require(isValidSignature( // <== QUESTA DOVREBBE PASSARE SE ABBIAMO DEFINITO CORRETTAMENTE L'HASH DA VERIFICARE SOPRA.
            order.maker,
            order.orderHash,
            v,
            r,
            s
        ));

        if (block.timestamp >= order.expirationTimestampInSec) {
            LogError(uint8(Errors.ORDER_EXPIRED), order.orderHash);
            return 0;
        }

        uint remainingTakerTokenAmount = safeSub(order.takerTokenAmount, getUnavailableTakerTokenAmount(order.orderHash));
        filledTakerTokenAmount = min256(fillTakerTokenAmount, remainingTakerTokenAmount);
        if (filledTakerTokenAmount == 0) {
            LogError(uint8(Errors.ORDER_FULLY_FILLED_OR_CANCELLED), order.orderHash);
            return 0;
        }

        if (isRoundingError(filledTakerTokenAmount, order.takerTokenAmount, order.makerTokenAmount)) {
            LogError(uint8(Errors.ROUNDING_ERROR_TOO_LARGE), order.orderHash);
            return 0;
        }

        if (!shouldThrowOnInsufficientBalanceOrAllowance && !isTransferable(order, filledTakerTokenAmount)) {
            LogError(uint8(Errors.INSUFFICIENT_BALANCE_OR_ALLOWANCE), order.orderHash);
            return 0;
        }

        uint filledMakerTokenAmount = getPartialAmount(filledTakerTokenAmount, order.takerTokenAmount, order.makerTokenAmount);
        uint paidMakerFee;
        uint paidTakerFee;
        filled[order.orderHash] = safeAdd(filled[order.orderHash], filledTakerTokenAmount);
        require(transferViaTokenTransferProxy(
            order.makerToken,
            tokenTransferProxyMaker, // <== SE SI TRATTA DI UN ORDINE MAKER DI UN FONDO, TRASFERRIAMO CORRETTAMENTE DAL FONDO AL TAKER
            msg.sender,
            filledMakerTokenAmount
        ));
        require(transferViaTokenTransferProxy(
            order.takerToken,
            msg.sender,
            tokenTransferProxyMaker, // <== SE SI TRATTA DI UN ORDINE MAKER DI UN FONDO, TRASFERRIAMO CORRETTAMENTE DAL FONDO AL TAKER
            filledTakerTokenAmount
        ));
        if (order.feeRecipient != address(0)) {
            if (order.makerFee > 0) {
                paidMakerFee = getPartialAmount(filledTakerTokenAmount, order.takerTokenAmount, order.makerFee);
                require(transferViaTokenTransferProxy(
                    ZRX_TOKEN_CONTRACT,
                    tokenTransferProxyMaker,
                    order.feeRecipient,
                    paidMakerFee
                ));
            }
            if (order.takerFee > 0) {
                paidTakerFee = getPartialAmount(filledTakerTokenAmount, order.takerTokenAmount, order.takerFee);
                require(transferViaTokenTransferProxy(
                    ZRX_TOKEN_CONTRACT,
                    msg.sender,
                    order.feeRecipient,
                    paidTakerFee
                ));
            }
        }

        LogFill(
            order.maker,
            msg.sender,
            order.feeRecipient,
            order.makerToken,
            order.takerToken,
            filledMakerTokenAmount,
            filledTakerTokenAmount,
            paidMakerFee,
            paidTakerFee,
            keccak256(order.makerToken, order.takerToken),
            order.orderHash
        );
        return filledTakerTokenAmount;
    }