Store scaled values and end of calldata in ERC1155 Asset Proxy

This commit is contained in:
Greg Hysen 2019-05-20 18:02:17 -07:00
parent 8453c616a5
commit 626d0dfa93
5 changed files with 229 additions and 34 deletions

View File

@ -77,7 +77,7 @@ contract ERC1155Proxy is
// | | 4 | | 1. from address |
// | | 36 | | 2. to address |
// | | 68 | | 3. offset to ids (*) |
// | | 100 | | 4. offset to values (*) |
// | | 100 | | 4. offset to scaledValues (*) |
// | | 132 | | 5. offset to data (*) |
// | Data | | | ids: |
// | | 164 | 32 | 1. ids Length |
@ -88,6 +88,9 @@ contract ERC1155Proxy is
// | | | | data |
// | | 228 + a + b | 32 | 1. data Length |
// | | 260 + a + b | c | 2. data Contents |
// | | | | scaledValues: (***) |
// | | 260 + a+b+c | 32 | 1. scaledValues Length |
// | | 292 + a+b+c | b | 2. scaledValues Contents |
//
//
// (*): offset is computed from start of function parameters, so offset
@ -97,11 +100,17 @@ contract ERC1155Proxy is
// offsets in the Data Area are dynamic and should be evaluated in
// real-time.
//
// (***): The contents of `values` are modified and stored separately, as `scaledValues`.
// The `values` array cannot be overwritten, as other dynamically allocated fields
// (`ids` and `data`) may resolve to the same array contents. For example, if
// `ids` = [1,2] and `values` = [1,2], the asset data may be optimized
// such that both arrays resolve to same entry of [1,2].
//
// WARNING: The ABIv2 specification allows additional padding between
// the Params and Data section. This will result in a larger
// offset to assetData.
//
// Note: Table #1 and Table #2 exists in Calldata. We construct Table #3 in memory.
// Note: Table #1 and Table #2 exist in Calldata. We construct Table #3 in memory.
//
//
assembly {
@ -177,17 +186,31 @@ contract ERC1155Proxy is
)
////////// STEP 2/4 //////////
let amount := calldataload(100)
// Setup iterators for `values` array (Table #3)
let valuesOffset := add(mload(100), 4) // add 4 for calldata offset
let valuesLengthInBytes := mul(
mload(valuesOffset),
32
)
let valuesLength := mload(valuesOffset)
let valuesLengthInBytes := mul(valuesLength, 32)
let valuesBegin := add(valuesOffset, 32)
let valuesEnd := add(valuesBegin, valuesLengthInBytes)
for { let tokenValueOffset := valuesBegin }
// Setup iterators for `scaledValues` array (Table #3).
// This array is placed at the end of the regular ERC1155 calldata,
// which is 32 bytes longer than `assetData` (Table #2).
let scaledValuesOffset := add(assetDataLength, 32)
let scaledValuesBegin := add(scaledValuesOffset, 32)
let scaledValuesEnd := add(scaledValuesBegin, valuesLengthInBytes)
// Scale `values` by `amount` and store the output in `scaledValues`
let amount := calldataload(100)
for {
let tokenValueOffset := valuesBegin
let scaledTokenValueOffset := scaledValuesBegin
}
lt(tokenValueOffset, valuesEnd)
{ tokenValueOffset := add(tokenValueOffset, 32) }
{
tokenValueOffset := add(tokenValueOffset, 32)
scaledTokenValueOffset := add(scaledTokenValueOffset, 32)
}
{
// Load token value and generate scaled value
let tokenValue := mload(tokenValueOffset)
@ -206,10 +229,17 @@ contract ERC1155Proxy is
revert(0, 100)
}
// There was no overflow, update `tokenValue` with its scaled counterpart
mstore(tokenValueOffset, scaledTokenValue)
// There was no overflow, store the scaled token value
mstore(scaledTokenValueOffset, scaledTokenValue)
}
// Store length of `scaledValues` (which is the same as `values`)
mstore(scaledValuesOffset, valuesLength)
// Point `values` to `scaledValues` (see Table #3);
// subtract 4 from memory location to account for selector
mstore(100, sub(scaledValuesOffset, 4))
////////// STEP 3/4 //////////
// Store the safeBatchTransferFrom function selector,
// and copy `from`/`to` fields from Table #1 to Table #3.
@ -232,7 +262,7 @@ contract ERC1155Proxy is
assetAddress, // call address of erc1155 asset
0, // don't send any ETH
0, // pointer to start of input
add(assetDataLength, 32), // length of input (Table #3) is 32 bytes longer than `assetData` (Table #2)
scaledValuesEnd, // length of input (Table #3) is the end of the `scaledValues`
0, // write output over memory that won't be reused
0 // don't copy output to memory
)

View File

@ -15,6 +15,7 @@ import {
web3Wrapper,
} from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { assetDataUtils } from '@0x/order-utils';
import { AssetProxyId, RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
@ -483,7 +484,15 @@ describe('ERC1155Proxy', () => {
const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
return value.times(valueMultiplier);
});
const erc1155ContractAddress = erc1155Wrapper.getContract().address;
const assetData = assetDataUtils.encodeERC1155AssetData(
erc1155ContractAddress,
tokensToTransfer,
valuesToTransfer,
receiverCallbackData,
);
const extraData = '0102030405060708';
const assetDataWithExtraData = `${assetData}${extraData}`;
// check balances before transfer
const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance];
await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
@ -497,7 +506,7 @@ describe('ERC1155Proxy', () => {
valueMultiplier,
receiverCallbackData,
authorized,
extraData,
assetDataWithExtraData,
);
// check receiver log ignored extra asset data
expect(txReceipt.logs.length).to.be.equal(2);
@ -519,6 +528,115 @@ describe('ERC1155Proxy', () => {
];
await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
});
it('should successfully transfer if token ids and values are abi encoded to same entry in calldata', async () => {
/**
* Suppose the `tokensToTransfer` and `valuesToTransfer` are identical; their offsets in
* the ABI-encoded asset data may be the same. E.g. token IDs [1, 2] and values [1, 2].
* Suppose we scale by a factor of 2, then we expect to trade token IDs [1, 2] and values [2, 4].
* This test ensures that scaling the values does not simultaneously scale the token IDs.
*/
///// Step 1/5 /////
// Create tokens with ids [1, 2, 3, 4] and mint a balance of 4 for the `spender`
const tokensToCreate = [new BigNumber(1), new BigNumber(2), new BigNumber(3), new BigNumber(4)];
const spenderInitialBalance = new BigNumber(4);
const receiverInitialBalance = new BigNumber(0);
const tokenUri = '';
for (const tokenToCreate of tokensToCreate) {
// create token
await erc1155Wrapper.getContract().createWithType.awaitTransactionSuccessAsync(
tokenToCreate,
tokenUri,
{
from: owner,
},
constants.AWAIT_TRANSACTION_MINED_MS,
);
// mint balance for spender
await erc1155Wrapper.getContract().mintFungible.awaitTransactionSuccessAsync(
tokenToCreate,
[spender],
[spenderInitialBalance],
{
from: owner,
},
constants.AWAIT_TRANSACTION_MINED_MS,
);
}
///// Step 2/5 /////
// Check balances before transfer
const balanceHolders = [spender, spender, spender, spender, receiver, receiver, receiver, receiver];
const balanceTokens = tokensToCreate.concat(tokensToCreate);
const initialBalances = await erc1155Wrapper.getBalancesAsync(balanceHolders, balanceTokens);
const expectedInitialBalances = [
spenderInitialBalance, // Token ID 1 / Spender Balance
spenderInitialBalance, // Token ID 2 / Spender Balance
spenderInitialBalance, // Token ID 3 / Spender Balance
spenderInitialBalance, // Token ID 4 / Spender Balance
receiverInitialBalance, // Token ID 1 / Receiver Balance
receiverInitialBalance, // Token ID 2 / Receiver Balance
receiverInitialBalance, // Token ID 3 / Receiver Balance
receiverInitialBalance, // Token ID 4 / Receiver Balance
];
expect(initialBalances).to.be.deep.equal(expectedInitialBalances);
///// Step 3/5 /////
// Create optimized calldata. We expect it to be formatted like the table below.
// 0x 0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082 // ERC1155 contract address
// 0x20 0000000000000000000000000000000000000000000000000000000000000080 // Offset to token IDs
// 0x40 0000000000000000000000000000000000000000000000000000000000000080 // Offset to token values (same as IDs)
// 0x60 00000000000000000000000000000000000000000000000000000000000000e0 // Offset to data
// 0x80 0000000000000000000000000000000000000000000000000000000000000002 // Length of token Ids / token values
// 0xA0 0000000000000000000000000000000000000000000000000000000000000001 // First Token ID / Token value
// 0xC0 0000000000000000000000000000000000000000000000000000000000000002 // Second Token ID / Token value
// 0xE0 0000000000000000000000000000000000000000000000000000000000000004 // Length of callback data
// 0x100 0102030400000000000000000000000000000000000000000000000000000000 // Callback data
const erc1155ContractAddress = erc1155Wrapper.getContract().address;
const tokensToTransfer = [new BigNumber(1), new BigNumber(2)];
const valuesToTransfer = tokensToTransfer;
const valueMultiplier = new BigNumber(2);
const assetData = assetDataUtils.encodeERC1155AssetData(
erc1155ContractAddress,
tokensToTransfer,
valuesToTransfer,
receiverCallbackData,
);
const offsetToTokenIds = 74;
const assetDataWithoutContractAddress = assetData.substr(offsetToTokenIds);
const expectedAssetDataWithoutContractAddress =
'0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040102030400000000000000000000000000000000000000000000000000000000';
expect(assetDataWithoutContractAddress).to.be.equal(expectedAssetDataWithoutContractAddress);
///// Step 4/5 /////
// Transfer token IDs [1, 2] and amounts [1, 2] with a multiplier of 2;
// the expected trade will be token IDs [1, 2] and amounts [2, 4]
await erc1155ProxyWrapper.transferFromAsync(
spender,
receiver,
erc1155Contract.address,
tokensToTransfer,
valuesToTransfer,
valueMultiplier,
receiverCallbackData,
authorized,
assetData,
);
///// Step 5/5 /////
// Validate final balances
const finalBalances = await erc1155Wrapper.getBalancesAsync(balanceHolders, balanceTokens);
const expectedAmountsTransferred = _.map(valuesToTransfer, value => {
return value.times(valueMultiplier);
});
const expectedFinalBalances = [
spenderInitialBalance.minus(expectedAmountsTransferred[0]), // Token ID 1 / Spender Balance
spenderInitialBalance.minus(expectedAmountsTransferred[1]), // Token ID 2 / Spender Balance
spenderInitialBalance, // Token ID 3 / Spender Balance
spenderInitialBalance, // Token ID 4 / Spender Balance
receiverInitialBalance.plus(expectedAmountsTransferred[0]), // Token ID 1 / Receiver Balance
receiverInitialBalance.plus(expectedAmountsTransferred[1]), // Token ID 2 / Receiver Balance
receiverInitialBalance, // Token ID 3 / Receiver Balance
receiverInitialBalance, // Token ID 4 / Receiver Balance
];
expect(finalBalances).to.be.deep.equal(expectedFinalBalances);
});
it('should transfer nothing if value is zero', async () => {
// setup test parameters
const tokenHolders = [spender, receiver];

View File

@ -106,20 +106,20 @@ export class ERC1155ProxyWrapper {
valueMultiplier: BigNumber,
receiverCallbackData: string,
authorizedSender: string,
extraData?: string,
assetData_?: string,
): Promise<string> {
this._validateProxyContractExistsOrThrow();
let encodedAssetData = assetDataUtils.encodeERC1155AssetData(
const assetData =
assetData_ === undefined
? assetDataUtils.encodeERC1155AssetData(
contractAddress,
tokensToTransfer,
valuesToTransfer,
receiverCallbackData,
);
if (extraData !== undefined) {
encodedAssetData = `${encodedAssetData}${extraData}`;
}
)
: assetData_;
const data = this._assetProxyInterface.transferFrom.getABIEncodedTransactionData(
encodedAssetData,
assetData,
from,
to,
valueMultiplier,
@ -128,6 +128,7 @@ export class ERC1155ProxyWrapper {
to: (this._proxyContract as ERC1155ProxyContract).address,
data,
from: authorizedSender,
gas: 300000,
});
return txHash;
}
@ -153,7 +154,7 @@ export class ERC1155ProxyWrapper {
valueMultiplier: BigNumber,
receiverCallbackData: string,
authorizedSender: string,
extraData?: string,
assetData?: string,
): Promise<TransactionReceiptWithDecodedLogs> {
const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(
await this.transferFromAsync(
@ -165,7 +166,7 @@ export class ERC1155ProxyWrapper {
valueMultiplier,
receiverCallbackData,
authorizedSender,
extraData,
assetData,
),
);
return txReceipt;

View File

@ -63,6 +63,32 @@ contract ERC1155Mintable is
}
}
/// @dev creates a new token
/// @param type_ of token
/// @param uri URI of token
function createWithType(
uint256 type_,
string calldata uri
)
external
{
// This will allow restricted access to creators.
creators[type_] = msg.sender;
// emit a Transfer event with Create semantic to help with discovery.
emit TransferSingle(
msg.sender,
address(0x0),
address(0x0),
type_,
0
);
if (bytes(uri).length > 0) {
emit URI(uri, type_);
}
}
/// @dev mints fungible tokens
/// @param id token type
/// @param to beneficiaries of minted tokens

View File

@ -13274,7 +13274,7 @@ prebuild-install@^2.2.2:
pump "^2.0.1"
rc "^1.1.6"
simple-get "^2.7.0"
tar-fs "~1.16.3"
tar-fs "^1.13.0"
tunnel-agent "^0.6.0"
which-pm-runs "^1.0.0"
@ -13854,7 +13854,7 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
version "1.2.6"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092"
dependencies:
deep-extend "^0.6.0"
deep-extend "~0.4.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
@ -13884,6 +13884,15 @@ react-dom@^16.3.2:
object-assign "^4.1.1"
prop-types "^15.6.0"
react-dom@^16.4.2:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.13.6"
react-dom@^16.5.2:
version "16.5.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
@ -13941,6 +13950,8 @@ react-highlight@0xproject/react-highlight#react-peer-deps:
dependencies:
highlight.js "^9.11.0"
highlightjs-solidity "^1.0.5"
react "^16.4.2"
react-dom "^16.4.2"
react-hot-loader@^4.3.3:
version "4.3.4"
@ -14185,6 +14196,15 @@ react@^16.3.2:
object-assign "^4.1.1"
prop-types "^15.6.0"
react@^16.4.2:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.13.6"
react@^16.5.2:
version "16.5.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
@ -15066,7 +15086,7 @@ schedule@^0.5.0:
scheduler@^0.13.6:
version "0.13.6"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"