Simplified the dydx bridge implememtation that does not use the bridge as the maker.

This commit is contained in:
Greg Hysen
2019-11-22 20:51:25 -08:00
parent 56cbb69401
commit 444125a7e1
8 changed files with 495 additions and 402 deletions

View File

@@ -64,6 +64,10 @@
{
"note": "Implement `KyberBridge`.",
"pr": 2352
},
{
"note": "Implement `DydxBridge`.",
"pr": 2365
}
],
"timestamp": 1575290197

View File

@@ -1,293 +0,0 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol";
import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "@0x/contracts-utils/contracts/src/Authorizable.sol";
import "../interfaces/IERC20Bridge.sol";
import "../interfaces/IDydx.sol";
import "../interfaces/IAssetData.sol";
// solhint-disable space-after-comma
contract DydxBridge is
IERC20Bridge,
DeploymentConstants,
Authorizable
{
using LibBytes for bytes;
// Return values for `isValidSignature`
// bytes4(keccak256('isValidSignature(bytes,bytes)'))
bytes4 constant VALID_SIGNATURE_RETURN_VALUE = bytes4(0x20c13b0b);
bytes4 constant INVALID_SIGNATURE_RETURN_VALUE = bytes4(0);
// OrderWithHash(LibOrder.Order order, bytes32 orderHash).selector
// == bytes4(keccak256('OrderWithHash((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes),bytes32)'))
bytes4 constant EIP1271_ORDER_WITH_HASH_SELECTOR = bytes4(0x3efe50c8);
struct BridgeData {
// Fields used by dydx
address dydxAccountOwner; // The owner of the dydx account.
uint256 dydxAccountNumber; // Account number used to identify the owner's specific account.
address dydxAccountOperator; // Operator of dydx account who signed the order (if transferred by an operator).
uint256 dydxFromMarketId; // Market ID of `from` asset.
uint256 dydxToMarketId; // Market ID of `to` asset.
// Fields used by bridge
bool shouldDepositIntodydx; // True iff contract balance should be deposited into dydx account.
address fromTokenAddress; // The token given to `from` or deposited into the dydx account.
}
/// @dev Callback for `IERC20Bridge`.
/// Function Prerequisite:
/// 1. Tokens are held in this contract that correspond to `dydxFromMarketId`, and
/// 2. Tokens are held in a dydx account that correspond to `dydxToMarketId`
///
/// When called, two actions take place:
/// 1. The total balance held by this contract is either (i) deposited into the dydx account OR (ii) transferred to `from`.
/// This is dictated by `BridgeData.shouldDepositIntoDydx`.
/// 2. Some `amount` of tokens are withdrawn from the dydx account into the address `to`.
///
/// Notes:
/// 1. This bridge must be set as an operator of the dydx account that is being operated on.
/// 2. This function may only be called in the context of the 0x Exchange executing an order (ERC20Bridge is authorized).
/// 3. The order must be signed by the owner or an operator of the dydx account. This is validated in `isValidSignature`.
/// @param from The sender of the tokens.
/// @param to The recipient of the tokens.
/// @param amount Minimum amount of `toTokenAddress` tokens to buy.
/// @param encodedBridgeData An abi-encoded `BridgeData` struct.
/// @return success The magic bytes if successful.
function bridgeTransferFrom(
address,
address from,
address to,
uint256 amount,
bytes calldata encodedBridgeData
)
external
onlyAuthorized
returns (bytes4 success)
{
// Decode bridge data.
(BridgeData memory bridgeData) = abi.decode(encodedBridgeData, (BridgeData));
// Cache dydx contract.
IDydx dydx = IDydx(_getDydxAddress());
// Cache the balance held by this contract.
IERC20Token fromToken = IERC20Token(bridgeData.fromTokenAddress);
uint256 fromTokenAmount = fromToken.balanceOf(address(this));
uint256 toTokenAmount = amount;
// Construct dydx account info.
IDydx.AccountInfo[] memory accounts = new IDydx.AccountInfo[](1);
accounts[0] = IDydx.AccountInfo({
owner: bridgeData.dydxAccountOwner,
number: bridgeData.dydxAccountNumber
});
// Construct arguments to `dydx.operate`.
IDydx.ActionArgs[] memory actions;
if (bridgeData.shouldDepositIntodydx) {
// Generate deposit/withdraw actions
actions = new IDydx.ActionArgs[](2);
actions[0] = _createDepositAction(
address(this), // deposit `fromToken` into dydx from this contract.
fromTokenAmount, // amount to deposit.
bridgeData // bridge data.
);
actions[1] = _createWithdrawAction(
to, // withdraw `toToken` from dydx to `to`.
toTokenAmount, // amount to withdraw.
bridgeData // bridge data.
);
// Allow dydx to deposit `fromToken` from this contract.
LibERC20Token.approve(
bridgeData.fromTokenAddress,
address(dydx),
uint256(-1)
);
} else {
// Generate withdraw action
actions = new IDydx.ActionArgs[](1);
actions[0] = _createWithdrawAction(to, toTokenAmount, bridgeData);
// Transfer `fromToken` to `from`
require(
fromToken.transfer(from, fromTokenAmount),
"TRANSFER_OF_FROM_TOKEN_FAILED"
);
}
// Run operations. This will revert on failure.
dydx.operate(accounts, actions);
return BRIDGE_SUCCESS;
}
/// @dev Returns a dydx `DepositAction`.
function _createDepositAction(
address depositFrom,
uint256 amount,
BridgeData memory bridgeData
)
internal
pure
returns (IDydx.ActionArgs memory)
{
// Construct action to deposit tokens held by this contract into dydx.
IDydx.AssetAmount memory amountToDeposit = IDydx.AssetAmount({
sign: true, // true if positive.
denomination: IDydx.AssetDenomination.Wei, // Wei => actual token amount held in account.
ref: IDydx.AssetReference.Target, // Target => an absolute amount.
value: amount // amount to deposit.
});
IDydx.ActionArgs memory depositAction = IDydx.ActionArgs({
actionType: IDydx.ActionType.Deposit, // deposit tokens.
amount: amountToDeposit, // amount to deposit.
accountId: 0, // index in the `accounts` when calling `operate` below.
primaryMarketId: bridgeData.dydxFromMarketId, // indicates which token to deposit.
otherAddress: depositFrom, // deposit tokens from `this` address.
// unused parameters
secondaryMarketId: 0,
otherAccountId: 0,
data: hex''
});
return depositAction;
}
/// @dev Returns a dydx `WithdrawAction`.
function _createWithdrawAction(
address withdrawTo,
uint256 amount,
BridgeData memory bridgeData
)
internal
pure
returns (IDydx.ActionArgs memory)
{
// Construct action to withdraw tokens from dydx into `to`.
IDydx.AssetAmount memory amountToWithdraw = IDydx.AssetAmount({
sign: true, // true if positive.
denomination: IDydx.AssetDenomination.Wei, // Wei => actual token amount held in account.
ref: IDydx.AssetReference.Target, // Target => an absolute amount.
value: amount // amount to withdraw.
});
IDydx.ActionArgs memory withdrawAction = IDydx.ActionArgs({
actionType: IDydx.ActionType.Withdraw, // withdraw tokens.
amount: amountToWithdraw, // amount to withdraw.
accountId: 0, // index in the `accounts` when calling `operate` below.
primaryMarketId: bridgeData.dydxToMarketId, // indicates which token to withdraw.
otherAddress: withdrawTo, // withdraw tokens to `to` address.
// unused parameters
secondaryMarketId: 0,
otherAccountId: 0,
data: hex''
});
return withdrawAction;
}
/// @dev Given a 0x order where the `makerAssetData` corresponds to a dydx transfer via this bridge,
/// `isValidSignature` verifies that the corresponding dydx account owner (or operator)
/// has authorized the trade by signing the input order.
/// @param data Signed tuple (ZeroExOrder, hash(ZeroExOrder))
/// @param signature Proof that `data` has been signed.
/// @return bytes4(0x20c13b0b) if the signature check succeeds.
function isValidSignature(
bytes calldata data,
bytes calldata signature
)
external
view
returns (bytes4)
{
// Assert that `data` is an encoded `OrderWithHash`.
require(
data.readBytes4(0) == EIP1271_ORDER_WITH_HASH_SELECTOR,
"INVALID_DATA_EXPECTED_EIP1271_ORDER_WITH_HASH"
);
// Assert that signature is correct length.
require(
signature.length == 65,
"INVALID_SIGNATURE_LENGTH"
);
// Decode the order and hash, plus extract the dydxBridge asset data.
(
LibOrder.Order memory order,
bytes32 orderHash
) = abi.decode(
data.slice(4, data.length),
(LibOrder.Order, bytes32)
);
// Decode and validate the asset proxy id.
require(
order.makerAssetData.readBytes4(0) == IAssetData(address(0)).ERC20Bridge.selector,
"MAKER_ASSET_DATA_NOT_ENCODED_FOR_ERC20_BRIDGE"
);
// Decode the ERC20 Bridge asset data.
(
/* address tokenAddress */,
address bridgeAddress,
bytes memory encodedBridgeData
) = abi.decode(
order.makerAssetData.slice(4, order.makerAssetData.length),
(address, address, bytes)
);
require(
bridgeAddress == address(this),
"INVALID_BRIDGE_ADDRESS"
);
// Decode and validate the `bridgeData` and extract the expected signer address.
(BridgeData memory bridgeData) = abi.decode(encodedBridgeData, (BridgeData));
address signerAddress = bridgeData.dydxAccountOperator != address(0)
? bridgeData.dydxAccountOperator
: bridgeData.dydxAccountOwner;
// Validate signature.
address recovered = ecrecover(
keccak256(abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
orderHash
)),
uint8(signature[0]), // v
signature.readBytes32(1), // r
signature.readBytes32(33) // s
);
// Return `VALID_SIGNATURE_RETURN_VALUE` iff signature is valid.
return (signerAddress == recovered)
? VALID_SIGNATURE_RETURN_VALUE
: INVALID_SIGNATURE_RETURN_VALUE;
}
}

View File

@@ -0,0 +1,179 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "@0x/contracts-utils/contracts/src/Authorizable.sol";
import "../interfaces/IERC20Bridge.sol";
import "../interfaces/IDydxBridge.sol";
import "../interfaces/IDydx.sol";
contract DydxBridge is
IERC20Bridge,
IDydxBridge,
DeploymentConstants,
Authorizable
{
/// @dev Callback for `IERC20Bridge`. Deposits or withdraws tokens from a dydx account.
/// Notes:
/// 1. This bridge must be set as an operator of the dydx account that is being operated on.
/// 2. This function may only be called in the context of the 0x Exchange executing an order
/// (ERC20Bridge is authorized).
/// 3. The order must be signed by the owner or an operator of the dydx account.
/// This signature validated by the 0x Exchange.
/// 4. The `from` address must be the maker (and hence is the owner or operator of the dydx account).
/// This is asserted during execution of this function.
/// @param from The sender of the tokens.
/// @param to The recipient of the tokens.
/// @param amount Minimum amount of `toTokenAddress` tokens to buy.
/// @param encodedBridgeData An abi-encoded `BridgeData` struct.
/// @return success The magic bytes if successful.
function bridgeTransferFrom(
address,
address from,
address to,
uint256 amount,
bytes calldata encodedBridgeData
)
external
onlyAuthorized
returns (bytes4 success)
{
// Decode bridge data.
(BridgeData memory bridgeData) = abi.decode(encodedBridgeData, (BridgeData));
// Cache dydx contract.
IDydx dydx = IDydx(_getDydxAddress());
// Assert that `from` is the owner or an operator of the dydx account.
require(
from == bridgeData.accountOwner || dydx.getIsLocalOperator(bridgeData.accountOwner, from),
"INVALID_DYDX_OWNER_OR_OPERATOR"
);
// Construct dydx account info.
IDydx.AccountInfo[] memory accounts = new IDydx.AccountInfo[](1);
accounts[0] = IDydx.AccountInfo({
owner: bridgeData.accountOwner,
number: bridgeData.accountNumber
});
// Create dydx action.
IDydx.ActionArgs[] memory actions = new IDydx.ActionArgs[](1);
if (bridgeData.action == BridgeAction.Deposit) {
actions[0] = _createDepositAction(
from,
amount,
bridgeData
);
} else if (bridgeData.action == BridgeAction.Withdraw) {
actions[0] = _createWithdrawAction(
to,
amount,
bridgeData
);
} else {
// If all values in the `Action` enum are handled then this
// revert is unreachable: Solidity will revert when casting
// from `uint8` to `Action`.
revert("UNRECOGNIZED_ACTION");
}
// Run operation. This will revert on failure.
dydx.operate(accounts, actions);
return BRIDGE_SUCCESS;
}
/// @dev Returns a dydx `DepositAction`.
/// @param depositFrom Deposit tokens from this address.
/// @param amount of tokens to deposit.
/// @param bridgeData A `BridgeData` struct.
function _createDepositAction(
address depositFrom,
uint256 amount,
BridgeData memory bridgeData
)
internal
pure
returns (IDydx.ActionArgs memory)
{
// Create dydx amount.
IDydx.AssetAmount memory amountToDeposit = IDydx.AssetAmount({
sign: true, // true if positive.
denomination: IDydx.AssetDenomination.Wei, // Wei => actual token amount held in account.
ref: IDydx.AssetReference.Target, // Target => an absolute amount.
value: amount // amount to deposit.
});
// Create dydx deposit action.
IDydx.ActionArgs memory depositAction = IDydx.ActionArgs({
actionType: IDydx.ActionType.Deposit, // deposit tokens.
amount: amountToDeposit, // amount to deposit.
accountId: 0, // index in the `accounts` when calling `operate`.
primaryMarketId: bridgeData.marketId, // indicates which token to deposit.
otherAddress: depositFrom, // deposit from this address.
// unused parameters
secondaryMarketId: 0,
otherAccountId: 0,
data: hex''
});
return depositAction;
}
/// @dev Returns a dydx `WithdrawAction`.
/// @param withdrawTo Withdraw tokens to this address.
/// @param amount of tokens to withdraw.
/// @param bridgeData A `BridgeData` struct.
function _createWithdrawAction(
address withdrawTo,
uint256 amount,
BridgeData memory bridgeData
)
internal
pure
returns (IDydx.ActionArgs memory)
{
// Create dydx amount.
IDydx.AssetAmount memory amountToWithdraw = IDydx.AssetAmount({
sign: true, // true if positive.
denomination: IDydx.AssetDenomination.Wei, // Wei => actual token amount held in account.
ref: IDydx.AssetReference.Target, // Target => an absolute amount.
value: amount // amount to withdraw.
});
// Create withdraw action.
IDydx.ActionArgs memory withdrawAction = IDydx.ActionArgs({
actionType: IDydx.ActionType.Withdraw, // withdraw tokens.
amount: amountToWithdraw, // amount to withdraw.
accountId: 0, // index in the `accounts` when calling `operate`.
primaryMarketId: bridgeData.marketId, // indicates which token to withdraw.
otherAddress: withdrawTo, // withdraw tokens to this address.
// unused parameters
secondaryMarketId: 0,
otherAccountId: 0,
data: hex''
});
return withdrawAction;
}
}

View File

@@ -87,13 +87,17 @@ interface IDydx {
)
external;
/// @dev Get the ERC20 token address for a market.
/// @param marketId The market to query
/// @return The token address
function getMarketTokenAddress(
uint256 marketId
///
/// @dev Return true if a particular address is approved as an operator for an owner's accounts.
/// Approved operators can act on the accounts of the owner as if it were the operator's own.
/// @param owner The owner of the accounts
/// @param operator The possible operator
/// @return True if operator is approved for owner's accounts
function getIsLocalOperator(
address owner,
address operator
)
external
view
returns (address);
returns (bool);
}

View File

@@ -0,0 +1,35 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
interface IDydxBridge {
enum BridgeAction {
Deposit, // Deposit tokens into dydx account.
Withdraw // Withdraw tokens from dydx account.
}
struct BridgeData {
BridgeAction action; // Action to run on dydx account.
address accountOwner; // The owner of the dydx account.
uint256 accountNumber; // Account number used to identify the owner's specific account.
uint256 marketId; // Market to operate on.
}
}

View File

@@ -19,17 +19,93 @@
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "../src/bridges/DydxBridge.sol";
contract TestDydxBridge {
// solhint-disable space-after-comma
contract TestDydxBridge is
IDydx,
DydxBridge
{
function OrderWithHash(
LibOrder.Order calldata order,
bytes32 orderHash
address public accountOperator;
constructor(address _accountOperator)
public
DydxBridge()
{
accountOperator = _accountOperator;
}
event OperateAccount(
address owner,
uint256 number
);
event OperateAction(
ActionType actionType,
uint256 accountId,
bool amountSign,
AssetDenomination amountDenomination,
AssetReference amountRef,
uint256 amountValue,
uint256 primaryMarketId,
uint256 secondaryMarketId,
address otherAddress,
uint256 otherAccountId,
bytes data
);
/// @dev Simulates `operate` in dydx contract.
/// Emits events so that arguments can be validated client-side.
function operate(
AccountInfo[] calldata accounts,
ActionArgs[] calldata actions
)
external
pure
{}
{
for (uint i = 0; i < accounts.length; ++i) {
emit OperateAccount(
accounts[i].owner,
accounts[i].number
);
}
for (uint i = 0; i < actions.length; ++i) {
emit OperateAction(
actions[i].actionType,
actions[i].accountId,
actions[i].amount.sign,
actions[i].amount.denomination,
actions[i].amount.ref,
actions[i].amount.value,
actions[i].primaryMarketId,
actions[i].secondaryMarketId,
actions[i].otherAddress,
actions[i].otherAccountId,
actions[i].data
);
}
}
/// @dev Return true iff `operator` equals `accountOperator` in state.
function getIsLocalOperator(
address /* owner */,
address operator
)
external
view
returns (bool)
{
return operator == accountOperator;
}
/// @dev overrides `_getDydxAddress()` from `DeploymentConstants` to return this address.
function _getDydxAddress()
internal
view
returns (address)
{
return address(this);
}
}

View File

@@ -1,120 +1,204 @@
import {
blockchainTests,
constants,
expect,
getRandomInteger,
hexLeftPad,
hexRandom,
OrderFactory,
orderHashUtils,
randomAddress,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { blockchainTests, constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { AuthorizableRevertErrors } from '@0x/contracts-utils';
import { AssetProxyId } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { DecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import * as ethUtil from 'ethereumjs-util';
import { artifacts } from './artifacts';
import { TestDydxBridgeContract, TestDydxBridgeEvents } from './wrappers';
import { DydxBridgeContract, IAssetDataContract, TestDydxBridgeContract } from './wrappers';
blockchainTests.resets.only('Dydx unit tests', env => {
const dydxAccountNumber = new BigNumber(1);
const dydxFromMarketId = new BigNumber(2);
const dydxToMarketId = new BigNumber(3);
let testContract: DydxBridgeContract;
blockchainTests.resets('DydxBridge unit tests', env => {
const accountNumber = new BigNumber(1);
const marketId = new BigNumber(2);
let testContract: TestDydxBridgeContract;
let owner: string;
let dydxAccountOwner: string;
let bridgeDataEncoder: AbiEncoder.DataType;
let eip1271Encoder: TestDydxBridgeContract;
let assetDataEncoder: IAssetDataContract;
let orderFactory: OrderFactory;
let authorized: string;
let notAuthorized: string;
let accountOwner: string;
let accountOperator: string;
let notAccountOwnerNorOperator: string;
let receiver: string;
before(async () => {
// Get accounts
const accounts = await env.web3Wrapper.getAvailableAddressesAsync();
[
owner,
authorized,
notAuthorized,
accountOwner,
accountOperator,
notAccountOwnerNorOperator,
receiver,
] = accounts;
// Deploy dydx bridge
testContract = await DydxBridgeContract.deployFrom0xArtifactAsync(
artifacts.DydxBridge,
testContract = await TestDydxBridgeContract.deployFrom0xArtifactAsync(
artifacts.TestDydxBridge,
env.provider,
env.txDefaults,
artifacts,
accountOperator,
);
// Get accounts
const accounts = await env.web3Wrapper.getAvailableAddressesAsync();
[owner, dydxAccountOwner] = accounts;
const dydxAccountOwnerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(dydxAccountOwner)];
// Create order factory for dydx bridge
const chainId = await env.getChainIdAsync();
const defaultOrderParams = {
...constants.STATIC_ORDER_PARAMS,
makerAddress: testContract.address,
feeRecipientAddress: randomAddress(),
makerAssetData: constants.NULL_BYTES,
takerAssetData: constants.NULL_BYTES,
makerFeeAssetData: constants.NULL_BYTES,
takerFeeAssetData: constants.NULL_BYTES,
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
exchangeAddress: constants.NULL_ADDRESS,
chainId,
};
orderFactory = new OrderFactory(dydxAccountOwnerPrivateKey, defaultOrderParams);
// Create encoder for Bridge Data
bridgeDataEncoder = AbiEncoder.create([
{name: 'dydxAccountOwner', type: 'address'},
{name: 'dydxAccountNumber', type: 'uint256'},
{name: 'dydxAccountOperator', type: 'address'},
{name: 'dydxFromMarketId', type: 'uint256'},
{name: 'dydxToMarketId', type: 'uint256'},
{name: 'shouldDepositIntoDydx', type: 'bool'},
{name: 'fromTokenAddress', type: 'address'},
]);
// Create encoders
assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, env.provider);
eip1271Encoder = new TestDydxBridgeContract(constants.NULL_ADDRESS, env.provider);
// Authorize `authorized` account on `testContract`.
await testContract.addAuthorizedAddress(authorized).awaitTransactionSuccessAsync({ from: owner });
});
describe('isValidSignature()', () => {
const SUCCESS_BYTES = '0x20c13b0b';
it('returns success bytes if signature is valid', async () => {
// Construct valid bridge data for dydx account owner
const bridgeData = {
dydxAccountOwner,
dydxAccountNumber,
dydxAccountOperator: constants.NULL_ADDRESS,
dydxFromMarketId,
dydxToMarketId,
shouldDepositIntoDydx: false,
fromTokenAddress: constants.NULL_ADDRESS,
};
const encodedBridgeData = bridgeDataEncoder.encode(bridgeData);
// Construct valid order from dydx account owner
const makerAssetData = assetDataEncoder
.ERC20Bridge(
describe('bridgeTransferFrom()', () => {
interface BridgeData {
action: number;
accountOwner: string;
accountNumber: BigNumber;
marketId: BigNumber;
}
enum DydxBridgeActions {
Deposit,
Withdraw,
}
let defaultBridgeData: any;
let bridgeDataEncoder: AbiEncoder.DataType;
const callBridgeTransferFrom = async (
from: string,
bridgeData: BridgeData,
sender: string,
): Promise<string> => {
const returnValue = await testContract
.bridgeTransferFrom(
constants.NULL_ADDRESS,
testContract.address,
encodedBridgeData
from,
receiver,
new BigNumber(1),
bridgeDataEncoder.encode(bridgeData),
)
.getABIEncodedTransactionData()
const signedOrder = await orderFactory.newSignedOrderAsync({
makerAssetData,
});
const signedOrderHash = orderHashUtils.getOrderHashHex(signedOrder);
.callAsync({ from: sender });
return returnValue;
};
const callBridgeTransferFromAndVerifyEvents = async (
actionType: number,
actionAddress: string,
from: string,
bridgeData: BridgeData,
sender: string,
): Promise<void> => {
// Execute transaction.
const txReceipt = await testContract
.bridgeTransferFrom(
constants.NULL_ADDRESS,
from,
receiver,
new BigNumber(1),
bridgeDataEncoder.encode(bridgeData),
)
.awaitTransactionSuccessAsync({ from: sender });
// Encode `isValidSignature` parameters
const eip1271Data = eip1271Encoder.OrderWithHash(signedOrder, signedOrderHash).getABIEncodedTransactionData();
const eip1271Signature = ethUtil.bufferToHex(ethUtil.toBuffer(signedOrder.signature).slice(0, 65)); // pop signature type from end
// Verify `OperateAccount` event.
verifyEventsFromLogs(
txReceipt.logs,
[
{
owner: accountOwner,
number: accountNumber,
},
],
TestDydxBridgeEvents.OperateAccount,
);
// Validate signature
const result = await testContract.isValidSignature(eip1271Data, eip1271Signature).callAsync();
expect(result).to.eq(SUCCESS_BYTES);
// Verify `OperateAction` event.
const accountId = new BigNumber(0);
const positiveAmountSign = true;
const weiDenomination = 0;
const absoluteAmountRef = 1;
verifyEventsFromLogs(
txReceipt.logs,
[
{
actionType,
accountId,
amountSign: positiveAmountSign,
amountDenomination: weiDenomination,
amountRef: absoluteAmountRef,
amountValue: new BigNumber(1),
primaryMarketId: marketId,
secondaryMarketId: constants.ZERO_AMOUNT,
otherAddress: actionAddress,
otherAccountId: constants.ZERO_AMOUNT,
data: '0x',
},
],
TestDydxBridgeEvents.OperateAction,
);
};
before(async () => {
// Construct default bridge data
defaultBridgeData = {
action: DydxBridgeActions.Deposit as number,
accountOwner,
accountNumber,
marketId,
};
// Create encoder for bridge data
bridgeDataEncoder = AbiEncoder.create([
{ name: 'action', type: 'uint8' },
{ name: 'accountOwner', type: 'address' },
{ name: 'accountNumber', type: 'uint256' },
{ name: 'marketId', type: 'uint256' },
]);
});
it('succeeds if `from` owns the dydx account', async () => {
await callBridgeTransferFrom(accountOwner, defaultBridgeData, authorized);
});
it('succeeds if `from` operates the dydx account', async () => {
await callBridgeTransferFrom(accountOperator, defaultBridgeData, authorized);
});
it('reverts if `from` is neither the owner nor the operator of the dydx account', async () => {
const tx = callBridgeTransferFrom(notAccountOwnerNorOperator, defaultBridgeData, authorized);
const expectedError = 'INVALID_DYDX_OWNER_OR_OPERATOR';
return expect(tx).to.revertWith(expectedError);
});
it('succeeds when calling `operate` with the `deposit` action', async () => {
const depositAction = 0;
const depositFrom = accountOwner;
const bridgeData = {
...defaultBridgeData,
action: depositAction,
};
await callBridgeTransferFromAndVerifyEvents(
depositAction,
depositFrom,
accountOwner,
bridgeData,
authorized,
);
});
it('succeeds when calling `operate` with the `withdraw` action', async () => {
const withdrawAction = 1;
const withdrawTo = receiver;
const bridgeData = {
...defaultBridgeData,
action: withdrawAction,
};
await callBridgeTransferFromAndVerifyEvents(
withdrawAction,
withdrawTo,
accountOwner,
bridgeData,
authorized,
);
});
it('reverts if called by an unauthorized account', async () => {
const callBridgeTransferFromPromise = callBridgeTransferFrom(
accountOwner,
defaultBridgeData,
notAuthorized,
);
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
return expect(callBridgeTransferFromPromise).to.revertWith(expectedError);
});
it('should return magic bytes if call succeeds', async () => {
const returnValue = await callBridgeTransferFrom(accountOwner, defaultBridgeData, authorized);
expect(returnValue).to.equal(AssetProxyId.ERC20Bridge);
});
});
});

View File

@@ -4,6 +4,10 @@
"changes": [
{
"note": "Add `DEV_UTILS_ADDRESS` and `KYBER_ETH_ADDRESS` to `DeploymentConstants`.",
"pr": 2395
},
{
"note": "Add `DydxBridge` to `DeploymentConstants`.",
"pr": 2365
}
]