Merge pull request #2440 from 0xProject/feat/dev-utils/chai-bridge-validation

Add ChaiBridge order balance/allowance checks to DevUtils
This commit is contained in:
Amir Bandeali 2020-01-17 11:46:27 -08:00 committed by GitHub
commit 28d1f3eef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 578 additions and 72 deletions

View File

@ -18,10 +18,22 @@
pragma solidity ^0.5.9;
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
contract PotLike {
function chi() external returns (uint256);
function rho() external returns (uint256);
function drip() external returns (uint256);
function join(uint256) external;
function exit(uint256) external;
}
// The actual Chai contract can be found here: https://github.com/dapphub/chai
contract IChai {
contract IChai is
IERC20Token
{
/// @dev Withdraws Dai owned by `src`
/// @param src Address that owns Dai.
/// @param wad Amount of Dai to withdraw.
@ -30,4 +42,25 @@ contract IChai {
uint256 wad
)
external;
/// @dev Queries Dai balance of Chai holder.
/// @param usr Address of Chai holder.
/// @return Dai balance.
function dai(address usr)
external
returns (uint256);
/// @dev Queries the Pot contract used by the Chai contract.
function pot()
external
returns (PotLike);
/// @dev Deposits Dai in exchange for Chai
/// @param dst Address to receive Chai.
/// @param wad Amount of Dai to deposit.
function join(
address dst,
uint256 wad
)
external;
}

View File

@ -15,6 +15,7 @@ export {
UniswapBridgeContract,
KyberBridgeContract,
ChaiBridgeContract,
IChaiContract,
} from './wrappers';
export { ERC20Wrapper } from './erc20_wrapper';

View File

@ -37,14 +37,24 @@ contract DevUtils is
LibEIP712ExchangeDomain,
EthBalanceChecker
{
constructor (address _exchange)
constructor (
address _exchange,
address _chaiBridge
)
public
OrderValidationUtils(_exchange)
OrderValidationUtils(
_exchange,
_chaiBridge
)
OrderTransferSimulationUtils(_exchange)
LibEIP712ExchangeDomain(uint256(0), address(0)) // null args because because we only use constants
{}
function getOrderHash(LibOrder.Order memory order, uint256 chainId, address exchange)
function getOrderHash(
LibOrder.Order memory order,
uint256 chainId,
address exchange
)
public
pure
returns (bytes32 orderHash)

View File

@ -26,10 +26,14 @@ import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol";
import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
import "@0x/contracts-erc721/contracts/src/interfaces/IERC721Token.sol";
import "@0x/contracts-erc1155/contracts/src/interfaces/IERC1155.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IChai.sol";
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol";
contract LibAssetData {
contract LibAssetData is
DeploymentConstants
{
// 2^256 - 1
uint256 constant internal _MAX_UINT256 = uint256(-1);
@ -41,9 +45,13 @@ contract LibAssetData {
address internal _ERC721_PROXY_ADDRESS;
address internal _ERC1155_PROXY_ADDRESS;
address internal _STATIC_CALL_PROXY_ADDRESS;
address internal _CHAI_BRIDGE_ADDRESS;
// solhint-enable var-name-mixedcase
constructor (address _exchange)
constructor (
address _exchange,
address _chaiBridge
)
public
{
_EXCHANGE = IExchange(_exchange);
@ -51,6 +59,7 @@ contract LibAssetData {
_ERC721_PROXY_ADDRESS = _EXCHANGE.getAssetProxy(IAssetData(address(0)).ERC721Token.selector);
_ERC1155_PROXY_ADDRESS = _EXCHANGE.getAssetProxy(IAssetData(address(0)).ERC1155Assets.selector);
_STATIC_CALL_PROXY_ADDRESS = _EXCHANGE.getAssetProxy(IAssetData(address(0)).StaticCall.selector);
_CHAI_BRIDGE_ADDRESS = _chaiBridge;
}
/// @dev Returns the owner's balance of the assets(s) specified in
@ -62,7 +71,6 @@ contract LibAssetData {
/// @return Number of assets (or asset baskets) held by owner.
function getBalance(address ownerAddress, bytes memory assetData)
public
view
returns (uint256 balance)
{
// Get id of AssetProxy contract
@ -71,16 +79,8 @@ contract LibAssetData {
if (assetProxyId == IAssetData(address(0)).ERC20Token.selector) {
// Get ERC20 token address
address tokenAddress = assetData.readAddress(16);
balance = _erc20BalanceOf(tokenAddress, ownerAddress);
// Encode data for `balanceOf(ownerAddress)`
bytes memory balanceOfData = abi.encodeWithSelector(
IERC20Token(address(0)).balanceOf.selector,
ownerAddress
);
// Query balance
(bool success, bytes memory returnData) = tokenAddress.staticcall(balanceOfData);
balance = success && returnData.length == 32 ? returnData.readUint256(0) : 0;
} else if (assetProxyId == IAssetData(address(0)).ERC721Token.selector) {
// Get ERC721 token address and id
(, address tokenAddress, uint256 tokenId) = decodeERC721AssetData(assetData);
@ -94,6 +94,7 @@ contract LibAssetData {
(bool success, bytes memory returnData) = tokenAddress.staticcall(ownerOfCalldata);
address currentOwnerAddress = (success && returnData.length == 32) ? returnData.readAddress(12) : address(0);
balance = currentOwnerAddress == ownerAddress ? 1 : 0;
} else if (assetProxyId == IAssetData(address(0)).ERC1155Assets.selector) {
// Get ERC1155 token address, array of ids, and array of values
(, address tokenAddress, uint256[] memory tokenIds, uint256[] memory tokenValues,) = decodeERC1155AssetData(assetData);
@ -125,6 +126,7 @@ contract LibAssetData {
balance = scaledBalance;
}
}
} else if (assetProxyId == IAssetData(address(0)).StaticCall.selector) {
// Encode data for `staticCallProxy.transferFrom(assetData,...)`
bytes memory transferFromData = abi.encodeWithSelector(
@ -140,6 +142,17 @@ contract LibAssetData {
// Success means that the staticcall can be made an unlimited amount of times
balance = success ? _MAX_UINT256 : 0;
} else if (assetProxyId == IAssetData(address(0)).ERC20Bridge.selector) {
// Get address of ERC20 token and bridge contract
(, address tokenAddress, address bridgeAddress,) = decodeERC20BridgeAssetData(assetData);
if (tokenAddress == _getDaiAddress() && bridgeAddress == _CHAI_BRIDGE_ADDRESS) {
uint256 chaiBalance = _erc20BalanceOf(_getChaiAddress(), ownerAddress);
// Calculate Dai balance
balance = _convertChaiToDaiAmount(chaiBalance);
}
// Balance will be 0 if bridge is not supported
} else if (assetProxyId == IAssetData(address(0)).MultiAsset.selector) {
// Get array of values and array of assetDatas
(, uint256[] memory assetAmounts, bytes[] memory nestedAssetData) = decodeMultiAssetData(assetData);
@ -176,7 +189,6 @@ contract LibAssetData {
/// corresponding to the same-indexed element in the assetData input.
function getBatchBalances(address ownerAddress, bytes[] memory assetData)
public
view
returns (uint256[] memory balances)
{
uint256 length = assetData.length;
@ -197,7 +209,6 @@ contract LibAssetData {
/// @return Number of assets (or asset baskets) that the corresponding AssetProxy is authorized to spend.
function getAssetProxyAllowance(address ownerAddress, bytes memory assetData)
public
view
returns (uint256 allowance)
{
// Get id of AssetProxy contract
@ -243,6 +254,7 @@ contract LibAssetData {
// Query allowance
(bool success, bytes memory returnData) = tokenAddress.staticcall(allowanceData);
allowance = success && returnData.length == 32 ? returnData.readUint256(0) : 0;
} else if (assetProxyId == IAssetData(address(0)).ERC721Token.selector) {
// Get ERC721 token address and id
(, address tokenAddress, uint256 tokenId) = decodeERC721AssetData(assetData);
@ -268,6 +280,7 @@ contract LibAssetData {
// Allowance is 2^256 - 1 if `isApprovedForAll` returned true
allowance = _MAX_UINT256;
}
} else if (assetProxyId == IAssetData(address(0)).ERC1155Assets.selector) {
// Get ERC1155 token address
(, address tokenAddress, , , ) = decodeERC1155AssetData(assetData);
@ -282,9 +295,26 @@ contract LibAssetData {
// Query allowance
(bool success, bytes memory returnData) = tokenAddress.staticcall(isApprovedForAllData);
allowance = success && returnData.length == 32 && returnData.readUint256(0) == 1 ? _MAX_UINT256 : 0;
} else if (assetProxyId == IAssetData(address(0)).StaticCall.selector) {
// The StaticCallProxy does not require any approvals
allowance = _MAX_UINT256;
} else if (assetProxyId == IAssetData(address(0)).ERC20Bridge.selector) {
// Get address of ERC20 token and bridge contract
(, address tokenAddress, address bridgeAddress,) = decodeERC20BridgeAssetData(assetData);
if (tokenAddress == _getDaiAddress() && bridgeAddress == _CHAI_BRIDGE_ADDRESS) {
bytes memory allowanceData = abi.encodeWithSelector(
IERC20Token(address(0)).allowance.selector,
ownerAddress,
_CHAI_BRIDGE_ADDRESS
);
(bool success, bytes memory returnData) = _getChaiAddress().staticcall(allowanceData);
uint256 chaiAllowance = success && returnData.length == 32 ? returnData.readUint256(0) : 0;
// Dai allowance is unlimited if Chai allowance is unlimited
allowance = chaiAllowance == _MAX_UINT256 ? _MAX_UINT256 : _convertChaiToDaiAmount(chaiAllowance);
}
// Allowance will be 0 if bridge is not supported
}
// Allowance will be 0 if the assetProxyId is unknown
@ -298,7 +328,6 @@ contract LibAssetData {
/// element corresponding to the same-indexed element in the assetData input.
function getBatchAssetProxyAllowances(address ownerAddress, bytes[] memory assetData)
public
view
returns (uint256[] memory allowances)
{
uint256 length = assetData.length;
@ -316,7 +345,6 @@ contract LibAssetData {
/// of assets (or asset baskets) that the corresponding AssetProxy is authorized to spend.
function getBalanceAndAssetProxyAllowance(address ownerAddress, bytes memory assetData)
public
view
returns (uint256 balance, uint256 allowance)
{
balance = getBalance(ownerAddress, assetData);
@ -332,7 +360,6 @@ contract LibAssetData {
/// corresponding to the same-indexed element in the assetData input.
function getBatchBalancesAndAssetProxyAllowances(address ownerAddress, bytes[] memory assetData)
public
view
returns (uint256[] memory balances, uint256[] memory allowances)
{
balances = getBatchBalances(ownerAddress, assetData);
@ -613,6 +640,35 @@ contract LibAssetData {
);
}
/// @dev Decode ERC20Bridge asset data from the format described in the AssetProxy contract specification.
/// @param assetData AssetProxy-compliant asset data describing an ERC20Bridge asset
/// @return The ERC20BridgeProxy identifier, the address of the ERC20 token to transfer, the address
/// of the bridge contract, and extra data to be passed to the bridge contract.
function decodeERC20BridgeAssetData(bytes memory assetData)
public
pure
returns (
bytes4 assetProxyId,
address tokenAddress,
address bridgeAddress,
bytes memory bridgeData
)
{
assetProxyId = assetData.readBytes4(0);
require(
assetProxyId == IAssetData(address(0)).ERC20Bridge.selector,
"WRONG_PROXY_ID"
);
(tokenAddress, bridgeAddress, bridgeData) = abi.decode(
assetData.slice(4, assetData.length),
(address, address, bytes)
);
}
/// @dev Reverts if assetData is not of a valid format for its given proxy id.
/// @param assetData AssetProxy compliant asset data.
function revertIfInvalidAssetData(bytes memory assetData)
public
pure
@ -629,8 +685,50 @@ contract LibAssetData {
decodeMultiAssetData(assetData);
} else if (assetProxyId == IAssetData(address(0)).StaticCall.selector) {
decodeStaticCallAssetData(assetData);
} else if (assetProxyId == IAssetData(address(0)).ERC20Bridge.selector) {
decodeERC20BridgeAssetData(assetData);
} else {
revert("WRONG_PROXY_ID");
}
}
/// @dev Queries balance of an ERC20 token. Returns 0 if call was unsuccessful.
/// @param tokenAddress Address of ERC20 token.
/// @param ownerAddress Address of owner of ERC20 token.
/// @return balance ERC20 token balance of owner.
function _erc20BalanceOf(
address tokenAddress,
address ownerAddress
)
internal
view
returns (uint256 balance)
{
// Encode data for `balanceOf(ownerAddress)`
bytes memory balanceOfData = abi.encodeWithSelector(
IERC20Token(address(0)).balanceOf.selector,
ownerAddress
);
// Query balance
(bool success, bytes memory returnData) = tokenAddress.staticcall(balanceOfData);
balance = success && returnData.length == 32 ? returnData.readUint256(0) : 0;
return balance;
}
/// @dev Converts an amount of Chai into its equivalent Dai amount.
/// Also accumulates Dai from DSR if called after the last time it was collected.
/// @param chaiAmount Amount of Chai to converts.
function _convertChaiToDaiAmount(uint256 chaiAmount)
internal
returns (uint256 daiAmount)
{
PotLike pot = IChai(_getChaiAddress()).pot();
// Accumulate savings if called after last time savings were collected
uint256 chiMultiplier = (now > pot.rho())
? pot.drip()
: pot.chi();
daiAmount = LibMath.getPartialAmountFloor(chiMultiplier, 10**27, chaiAmount);
return daiAmount;
}
}

View File

@ -35,9 +35,15 @@ contract OrderValidationUtils is
using LibBytes for bytes;
using LibSafeMath for uint256;
constructor (address _exchange)
constructor (
address _exchange,
address _chaiBridge
)
public
LibAssetData(_exchange)
LibAssetData(
_exchange,
_chaiBridge
)
{}
/// @dev Fetches all order-relevant information needed to validate if the supplied order is fillable.
@ -173,7 +179,6 @@ contract OrderValidationUtils is
/// the individual asset amounts located within the `assetData`.
function getTransferableAssetAmount(address ownerAddress, bytes memory assetData)
public
view
returns (uint256 transferableAssetAmount)
{
(uint256 balance, uint256 allowance) = getBalanceAndAssetProxyAllowance(ownerAddress, assetData);

View File

@ -14,7 +14,7 @@ import { contractAddresses, dydxAccountOwner } from '../mainnet_fork_utils';
import { dydxEvents } from './abi/dydxEvents';
blockchainTests.fork.resets('Mainnet dydx bridge tests', env => {
blockchainTests.fork.skip('Mainnet dydx bridge tests', env => {
let testContract: DydxBridgeContract;
// random account to receive tokens from dydx
const receiver = '0x986ccf5234d9cfbb25246f1a5bfa51f4ccfcb308';

View File

@ -0,0 +1,76 @@
import { IChaiContract } from '@0x/contracts-asset-proxy';
import { artifacts as devUtilsArtifacts, DevUtilsContract } from '@0x/contracts-dev-utils';
import { ERC20TokenContract } from '@0x/contracts-erc20';
import { blockchainTests, constants, expect } from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { contractAddresses } from '../mainnet_fork_utils';
blockchainTests.fork.resets('DevUtils mainnet tests', env => {
const daiAddress = '0x6b175474e89094c44da98b954eedeac495271d0f';
const chaiAddress = '0x06af07097c9eeb7fd685c692751d5c66db49c215';
const daiHolder = '0x6cc5f688a315f3dc28a7781717a9a798a59fda7b';
let noDaiAddress: string;
const assetData = assetDataUtils.encodeERC20BridgeAssetData(
daiAddress,
contractAddresses.chaiBridge,
constants.NULL_BYTES,
);
const dai = new ERC20TokenContract(daiAddress, env.provider, env.txDefaults);
const chai = new IChaiContract(chaiAddress, env.provider, env.txDefaults);
let devUtils: DevUtilsContract;
const daiDepositAmount = Web3Wrapper.toBaseUnitAmount(1000, 18);
before(async () => {
[noDaiAddress] = await env.getAccountAddressesAsync();
devUtils = await DevUtilsContract.deployFrom0xArtifactAsync(
devUtilsArtifacts.DevUtils,
env.provider,
env.txDefaults,
devUtilsArtifacts,
contractAddresses.exchange,
contractAddresses.chaiBridge,
);
await dai.approve(chai.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: daiHolder });
await chai.join(daiHolder, daiDepositAmount).awaitTransactionSuccessAsync({ from: daiHolder });
});
describe('LibAssetData', () => {
describe('ChaiBridge getBalance', () => {
it('should return the correct non-zero Dai balance for a Chai holder', async () => {
const expectedDaiBalance = await chai.dai(daiHolder).callAsync();
const daiBalance = await devUtils.getBalance(daiHolder, assetData).callAsync();
expect(daiBalance).bignumber.eq(expectedDaiBalance);
});
it('should return a balance of 0 when Chai balance is 0', async () => {
const daiBalance = await devUtils.getBalance(noDaiAddress, assetData).callAsync();
expect(daiBalance).bignumber.eq(constants.ZERO_AMOUNT);
});
});
describe('ChaiBridge getProxyAllowance', () => {
it('should return the correct non-zero non-unlimited allowance', async () => {
const chaiBalance = await chai.balanceOf(daiHolder).callAsync();
await chai
.approve(contractAddresses.chaiBridge, chaiBalance)
.awaitTransactionSuccessAsync({ from: daiHolder });
const daiBalance = await chai.dai(daiHolder).callAsync();
const allowance = await devUtils.getAssetProxyAllowance(daiHolder, assetData).callAsync();
expect(allowance).to.bignumber.eq(daiBalance);
});
it('should return an unlimited allowance of Dai when Chai allowance is also unlimited', async () => {
await chai
.approve(contractAddresses.chaiBridge, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: daiHolder });
const allowance = await devUtils.getAssetProxyAllowance(daiHolder, assetData).callAsync();
expect(allowance).to.bignumber.eq(constants.MAX_UINT256);
});
it('should return an allowance of 0 when Chai allowance is 0', async () => {
const allowance = await devUtils.getAssetProxyAllowance(noDaiAddress, assetData).callAsync();
expect(allowance).to.bignumber.eq(constants.ZERO_AMOUNT);
});
});
});
});

View File

@ -25,6 +25,7 @@ blockchainTests('DevUtils.getOrderHash', env => {
env.txDefaults,
artifacts,
exchange.address,
constants.NULL_ADDRESS,
);
});

View File

@ -79,6 +79,7 @@ blockchainTests.resets('LibAssetData', env => {
env.txDefaults,
artifacts,
deployment.exchange.address,
constants.NULL_ADDRESS,
);
staticCallTarget = await TestStaticCallTargetContract.deployFrom0xArtifactAsync(

View File

@ -203,6 +203,7 @@ export class DeploymentManager {
environment.txDefaults,
devUtilsArtifacts,
exchange.address,
constants.NULL_ADDRESS,
);
const assetDataEncoder = new IAssetDataContract(constants.NULL_ADDRESS, environment.provider);

View File

@ -7,7 +7,7 @@ import { BigNumber } from '@0x/utils';
import { contractAddresses, getContractwrappers } from './mainnet_fork_utils';
blockchainTests.fork.resets('Mainnet configs tests', env => {
blockchainTests.live('Mainnet configs tests', env => {
let contractWrappers: ContractWrappers;
before(async () => {

View File

@ -20,6 +20,7 @@ export let providerConfigs: Web3Config = {
shouldUseInProcessGanache: true,
shouldAllowUnlimitedContractSize: true,
hardfork: 'istanbul',
unlocked_accounts: ['0x6cc5f688a315f3dc28a7781717a9a798a59fda7b', '0x55dc8f21d20d4c6ed3c82916a438a413ca68e335'],
};
export const provider: Web3ProviderEngine = web3Factory.getRpcProvider(providerConfigs);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -175,6 +175,7 @@ export async function runMigrationsAsync(
txDefaults,
artifacts,
exchange.address,
constants.NULL_ADDRESS,
);
// tslint:disable-next-line:no-unused-variable

View File

@ -113,7 +113,7 @@ export async function runMigrationsAsync(supportedProvider: SupportedProvider, t
assetProxyArtifacts,
);
await ChaiBridgeContract.deployFrom0xArtifactAsync(
const chaiBridge = await ChaiBridgeContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.ChaiBridge,
provider,
txDefaults,
@ -251,6 +251,7 @@ export async function runMigrationsAsync(supportedProvider: SupportedProvider, t
txDefaults,
devUtilsArtifacts,
exchange.address,
chaiBridge.address,
);
await CoordinatorContract.deployFrom0xArtifactAsync(