Open wnz99 opened 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
);
}
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;
}
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.
where the fields are:
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
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:
We will need to modify the above function in oder to include any additional fields, or we can just create our own hashing function.
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;
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:
Now let's actually verify whether the order we created is valid
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:
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
: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:3) We would change the hashing funtion in order to give the same output as the js amended funtion above:
We need to put the fund address some where in there.
The signature should validate correctly at the following line:
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...