* move orderParsingUtils from order-utils to connect * Remove many functions from signatureUtils Removed from the exported object, that is. All of them are used in other existing code, so they were all moved to be as local to their usage as possible. * remove orderHashUtils.isValidOrderHash() * Move all *RevertErrors from order-utils... ...into their respective @0x/contracts- packages. * Refactor @0x/order-utils' orderHashUtils away - Move existing routines into @0x/contracts-test-utils - Migrate non-contract-test callers to a newly-exposed getOrderHash() method in DevUtils. * Move all *RevertErrors from @0x/utils... ...into their respective @0x/contracts- packages. * rm transactionHashUtils.isValidTransactionHash() * DevUtils.sol: Fail yarn test if too big to deploy * Refactor @0x/order-utils transactionHashUtils away - Move existing routines into @0x/contracts-test-utils - Migrate non-contract-test callers to a newly-exposed getTransactionHash() method in DevUtils. * Consolidate `Removed export...` CHANGELOG entries * Rm EthBalanceChecker from devutils wrapper exports * Stop importing from '.' or '.../src' * fix builds * fix prettier; dangling promise * increase max bundle size
449 lines
21 KiB
TypeScript
449 lines
21 KiB
TypeScript
import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
|
|
import { DevUtilsContract } from '@0x/contracts-dev-utils';
|
|
import {
|
|
blockchainTests,
|
|
constants,
|
|
expect,
|
|
expectTransactionFailedAsync,
|
|
filterLogsToArguments,
|
|
provider,
|
|
} from '@0x/contracts-test-utils';
|
|
import { AuthorizableRevertErrors, SafeMathRevertErrors } from '@0x/contracts-utils';
|
|
import { RevertReason } from '@0x/types';
|
|
import { BigNumber } from '@0x/utils';
|
|
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
|
|
|
import { constants as stakingConstants } from '../../src/constants';
|
|
import StakingRevertErrors = require('../../src/staking_revert_errors');
|
|
|
|
import { artifacts } from '../artifacts';
|
|
import {
|
|
ZrxVaultContract,
|
|
ZrxVaultDepositEventArgs,
|
|
ZrxVaultInCatastrophicFailureModeEventArgs,
|
|
ZrxVaultStakingProxySetEventArgs,
|
|
ZrxVaultWithdrawEventArgs,
|
|
ZrxVaultZrxProxySetEventArgs,
|
|
} from '../wrappers';
|
|
|
|
blockchainTests.resets('ZrxVault unit tests', env => {
|
|
let accounts: string[];
|
|
let owner: string;
|
|
let nonOwnerAddresses: string[];
|
|
let erc20Wrapper: ERC20Wrapper;
|
|
let zrxVault: ZrxVaultContract;
|
|
let zrxAssetData: string;
|
|
let zrxProxyAddress: string;
|
|
|
|
const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider);
|
|
|
|
before(async () => {
|
|
// create accounts
|
|
accounts = await env.getAccountAddressesAsync();
|
|
[owner, ...nonOwnerAddresses] = accounts;
|
|
|
|
// set up ERC20Wrapper
|
|
erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner);
|
|
// deploy erc20 proxy
|
|
const erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
|
|
zrxProxyAddress = erc20ProxyContract.address;
|
|
// deploy zrx token
|
|
const [zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, constants.DUMMY_TOKEN_DECIMALS);
|
|
zrxAssetData = await devUtils.encodeERC20AssetData(zrxTokenContract.address).callAsync();
|
|
|
|
await erc20Wrapper.setBalancesAndAllowancesAsync();
|
|
|
|
zrxVault = await ZrxVaultContract.deployFrom0xArtifactAsync(
|
|
artifacts.ZrxVault,
|
|
env.provider,
|
|
env.txDefaults,
|
|
artifacts,
|
|
zrxProxyAddress,
|
|
zrxTokenContract.address,
|
|
);
|
|
|
|
await zrxVault.addAuthorizedAddress(owner).awaitTransactionSuccessAsync();
|
|
|
|
// configure erc20 proxy to accept calls from zrx vault
|
|
await erc20ProxyContract.addAuthorizedAddress(zrxVault.address).awaitTransactionSuccessAsync();
|
|
});
|
|
|
|
enum ZrxTransfer {
|
|
Deposit,
|
|
Withdrawal,
|
|
}
|
|
|
|
async function verifyTransferPostconditionsAsync(
|
|
transferType: ZrxTransfer,
|
|
staker: string,
|
|
amount: BigNumber,
|
|
initialVaultBalance: BigNumber,
|
|
initialTokenBalance: BigNumber,
|
|
receipt: TransactionReceiptWithDecodedLogs,
|
|
): Promise<void> {
|
|
const eventArgs =
|
|
transferType === ZrxTransfer.Deposit
|
|
? filterLogsToArguments<ZrxVaultDepositEventArgs>(receipt.logs, 'Deposit')
|
|
: filterLogsToArguments<ZrxVaultWithdrawEventArgs>(receipt.logs, 'Withdraw');
|
|
expect(eventArgs.length).to.equal(1);
|
|
expect(eventArgs[0].staker).to.equal(staker);
|
|
expect(eventArgs[0].amount).to.bignumber.equal(amount);
|
|
|
|
const newVaultBalance = await zrxVault.balanceOf(staker).callAsync();
|
|
const newTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
|
|
const [expectedVaultBalance, expectedTokenBalance] =
|
|
transferType === ZrxTransfer.Deposit
|
|
? [initialVaultBalance.plus(amount), initialTokenBalance.minus(amount)]
|
|
: [initialVaultBalance.minus(amount), initialTokenBalance.plus(amount)];
|
|
expect(newVaultBalance).to.bignumber.equal(expectedVaultBalance);
|
|
expect(newTokenBalance).to.bignumber.equal(expectedTokenBalance);
|
|
}
|
|
|
|
describe('Normal operation', () => {
|
|
describe('Setting proxies', () => {
|
|
async function verifyStakingProxySetAsync(
|
|
receipt: TransactionReceiptWithDecodedLogs,
|
|
newProxy: string,
|
|
): Promise<void> {
|
|
const eventArgs = filterLogsToArguments<ZrxVaultStakingProxySetEventArgs>(
|
|
receipt.logs,
|
|
'StakingProxySet',
|
|
);
|
|
expect(eventArgs.length).to.equal(1);
|
|
expect(eventArgs[0].stakingProxyAddress).to.equal(newProxy);
|
|
const actualAddress = await zrxVault.stakingProxyAddress().callAsync();
|
|
expect(actualAddress).to.equal(newProxy);
|
|
}
|
|
|
|
it('Owner can set the ZRX proxy', async () => {
|
|
const newProxy = nonOwnerAddresses[0];
|
|
const receipt = await zrxVault.setZrxProxy(newProxy).awaitTransactionSuccessAsync({
|
|
from: owner,
|
|
});
|
|
const eventArgs = filterLogsToArguments<ZrxVaultZrxProxySetEventArgs>(receipt.logs, 'ZrxProxySet');
|
|
expect(eventArgs.length).to.equal(1);
|
|
expect(eventArgs[0].zrxProxyAddress).to.equal(newProxy);
|
|
});
|
|
it('Authorized address can set the ZRX proxy', async () => {
|
|
const [authorized, newProxy] = nonOwnerAddresses;
|
|
await zrxVault.addAuthorizedAddress(authorized).awaitTransactionSuccessAsync({ from: owner });
|
|
const receipt = await zrxVault.setZrxProxy(newProxy).awaitTransactionSuccessAsync({
|
|
from: authorized,
|
|
});
|
|
const eventArgs = filterLogsToArguments<ZrxVaultZrxProxySetEventArgs>(receipt.logs, 'ZrxProxySet');
|
|
expect(eventArgs.length).to.equal(1);
|
|
expect(eventArgs[0].zrxProxyAddress).to.equal(newProxy);
|
|
});
|
|
it('Non-authorized address cannot set the ZRX proxy', async () => {
|
|
const [notAuthorized, newProxy] = nonOwnerAddresses;
|
|
const tx = zrxVault.setZrxProxy(newProxy).awaitTransactionSuccessAsync({
|
|
from: notAuthorized,
|
|
});
|
|
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
|
|
expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('Owner can set the staking proxy', async () => {
|
|
const newProxy = nonOwnerAddresses[0];
|
|
const receipt = await zrxVault.setStakingProxy(newProxy).awaitTransactionSuccessAsync({
|
|
from: owner,
|
|
});
|
|
await verifyStakingProxySetAsync(receipt, newProxy);
|
|
});
|
|
it('Authorized address can set the staking proxy', async () => {
|
|
const [authorized, newProxy] = nonOwnerAddresses;
|
|
await zrxVault.addAuthorizedAddress(authorized).awaitTransactionSuccessAsync({ from: owner });
|
|
const receipt = await zrxVault.setStakingProxy(newProxy).awaitTransactionSuccessAsync({
|
|
from: authorized,
|
|
});
|
|
await verifyStakingProxySetAsync(receipt, newProxy);
|
|
});
|
|
it('Non-authorized address cannot set the staking proxy', async () => {
|
|
const [notAuthorized, newProxy] = nonOwnerAddresses;
|
|
const tx = zrxVault.setStakingProxy(newProxy).awaitTransactionSuccessAsync({
|
|
from: notAuthorized,
|
|
});
|
|
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
|
|
expect(tx).to.revertWith(expectedError);
|
|
const actualAddress = await zrxVault.stakingProxyAddress().callAsync();
|
|
expect(actualAddress).to.equal(stakingConstants.NIL_ADDRESS);
|
|
});
|
|
});
|
|
describe('ZRX management', () => {
|
|
let staker: string;
|
|
let stakingProxy: string;
|
|
let initialVaultBalance: BigNumber;
|
|
let initialTokenBalance: BigNumber;
|
|
|
|
before(async () => {
|
|
[staker, stakingProxy] = nonOwnerAddresses;
|
|
await zrxVault.setStakingProxy(stakingProxy).awaitTransactionSuccessAsync({ from: owner });
|
|
await zrxVault.depositFrom(staker, new BigNumber(10)).awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
initialVaultBalance = await zrxVault.balanceOf(staker).callAsync();
|
|
initialTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
|
|
});
|
|
|
|
describe('Deposit', () => {
|
|
it('Staking proxy can deposit zero amount on behalf of staker', async () => {
|
|
const receipt = await zrxVault
|
|
.depositFrom(staker, constants.ZERO_AMOUNT)
|
|
.awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Deposit,
|
|
staker,
|
|
constants.ZERO_AMOUNT,
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
it('Staking proxy can deposit nonzero amount on behalf of staker', async () => {
|
|
const receipt = await zrxVault.depositFrom(staker, new BigNumber(1)).awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Deposit,
|
|
staker,
|
|
new BigNumber(1),
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
it('Staking proxy can deposit entire ZRX balance on behalf of staker', async () => {
|
|
const receipt = await zrxVault
|
|
.depositFrom(staker, initialTokenBalance)
|
|
.awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Deposit,
|
|
staker,
|
|
initialTokenBalance,
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
it("Reverts if attempting to deposit more than staker's ZRX balance", async () => {
|
|
const tx = zrxVault.depositFrom(staker, initialTokenBalance.plus(1)).sendTransactionAsync({
|
|
from: stakingProxy,
|
|
});
|
|
expectTransactionFailedAsync(tx, RevertReason.TransferFailed);
|
|
});
|
|
});
|
|
describe('Withdrawal', () => {
|
|
it('Staking proxy can withdraw zero amount on behalf of staker', async () => {
|
|
const receipt = await zrxVault
|
|
.withdrawFrom(staker, constants.ZERO_AMOUNT)
|
|
.awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Withdrawal,
|
|
staker,
|
|
constants.ZERO_AMOUNT,
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
it('Staking proxy can withdraw nonzero amount on behalf of staker', async () => {
|
|
const receipt = await zrxVault.withdrawFrom(staker, new BigNumber(1)).awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Withdrawal,
|
|
staker,
|
|
new BigNumber(1),
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
it('Staking proxy can withdraw entire vault balance on behalf of staker', async () => {
|
|
const receipt = await zrxVault
|
|
.withdrawFrom(staker, initialVaultBalance)
|
|
.awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Withdrawal,
|
|
staker,
|
|
initialVaultBalance,
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
it("Reverts if attempting to withdraw more than staker's vault balance", async () => {
|
|
const tx = zrxVault.withdrawFrom(staker, initialVaultBalance.plus(1)).awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
const expectedError = new SafeMathRevertErrors.Uint256BinOpError(
|
|
SafeMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow,
|
|
initialVaultBalance,
|
|
initialVaultBalance.plus(1),
|
|
);
|
|
expect(tx).to.revertWith(expectedError);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Catastrophic Failure Mode', () => {
|
|
describe('Authorization', () => {
|
|
async function verifyCatastrophicFailureModeAsync(
|
|
sender: string,
|
|
receipt: TransactionReceiptWithDecodedLogs,
|
|
): Promise<void> {
|
|
const eventArgs = filterLogsToArguments<ZrxVaultInCatastrophicFailureModeEventArgs>(
|
|
receipt.logs,
|
|
'InCatastrophicFailureMode',
|
|
);
|
|
expect(eventArgs.length).to.equal(1);
|
|
expect(eventArgs[0].sender).to.equal(sender);
|
|
expect(await zrxVault.isInCatastrophicFailure().callAsync()).to.be.true();
|
|
}
|
|
|
|
it('Owner can turn on Catastrophic Failure Mode', async () => {
|
|
const receipt = await zrxVault.enterCatastrophicFailure().awaitTransactionSuccessAsync({ from: owner });
|
|
await verifyCatastrophicFailureModeAsync(owner, receipt);
|
|
});
|
|
it('Authorized address can turn on Catastrophic Failure Mode', async () => {
|
|
const authorized = nonOwnerAddresses[0];
|
|
await zrxVault.addAuthorizedAddress(authorized).awaitTransactionSuccessAsync({ from: owner });
|
|
const receipt = await zrxVault.enterCatastrophicFailure().awaitTransactionSuccessAsync({
|
|
from: authorized,
|
|
});
|
|
await verifyCatastrophicFailureModeAsync(authorized, receipt);
|
|
});
|
|
it('Non-authorized address cannot turn on Catastrophic Failure Mode', async () => {
|
|
const notAuthorized = nonOwnerAddresses[0];
|
|
const tx = zrxVault.enterCatastrophicFailure().awaitTransactionSuccessAsync({
|
|
from: notAuthorized,
|
|
});
|
|
const expectedError = new AuthorizableRevertErrors.SenderNotAuthorizedError(notAuthorized);
|
|
expect(tx).to.revertWith(expectedError);
|
|
expect(await zrxVault.isInCatastrophicFailure().callAsync()).to.be.false();
|
|
});
|
|
it('Catastrophic Failure Mode can only be turned on once', async () => {
|
|
const authorized = nonOwnerAddresses[0];
|
|
await zrxVault.addAuthorizedAddress(authorized).awaitTransactionSuccessAsync({ from: owner });
|
|
await zrxVault.enterCatastrophicFailure().awaitTransactionSuccessAsync({
|
|
from: authorized,
|
|
});
|
|
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
|
|
return expect(zrxVault.enterCatastrophicFailure().awaitTransactionSuccessAsync()).to.revertWith(
|
|
expectedError,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Affected functionality', () => {
|
|
let staker: string;
|
|
let stakingProxy: string;
|
|
let initialVaultBalance: BigNumber;
|
|
let initialTokenBalance: BigNumber;
|
|
|
|
before(async () => {
|
|
[staker, stakingProxy, ...nonOwnerAddresses] = nonOwnerAddresses;
|
|
await zrxVault.setStakingProxy(stakingProxy).awaitTransactionSuccessAsync({ from: owner });
|
|
await zrxVault.depositFrom(staker, new BigNumber(10)).awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
await zrxVault.enterCatastrophicFailure().awaitTransactionSuccessAsync({ from: owner });
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
initialVaultBalance = await zrxVault.balanceOf(staker).callAsync();
|
|
initialTokenBalance = await erc20Wrapper.getBalanceAsync(staker, zrxAssetData);
|
|
});
|
|
|
|
it('Owner cannot set the ZRX proxy', async () => {
|
|
const newProxy = nonOwnerAddresses[0];
|
|
const tx = zrxVault.setZrxProxy(newProxy).awaitTransactionSuccessAsync({
|
|
from: owner,
|
|
});
|
|
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
|
|
expect(tx).to.revertWith(expectedError);
|
|
const actualAddress = await zrxVault.zrxAssetProxy().callAsync();
|
|
expect(actualAddress).to.equal(zrxProxyAddress);
|
|
});
|
|
it('Authorized address cannot set the ZRX proxy', async () => {
|
|
const [authorized, newProxy] = nonOwnerAddresses;
|
|
await zrxVault.addAuthorizedAddress(authorized).awaitTransactionSuccessAsync({ from: owner });
|
|
const tx = zrxVault.setZrxProxy(newProxy).awaitTransactionSuccessAsync({
|
|
from: authorized,
|
|
});
|
|
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
|
|
expect(tx).to.revertWith(expectedError);
|
|
const actualAddress = await zrxVault.zrxAssetProxy().callAsync();
|
|
expect(actualAddress).to.equal(zrxProxyAddress);
|
|
});
|
|
it('Staking proxy cannot deposit ZRX', async () => {
|
|
const tx = zrxVault.depositFrom(staker, new BigNumber(1)).awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
|
|
expect(tx).to.revertWith(expectedError);
|
|
});
|
|
|
|
describe('Withdrawal', () => {
|
|
it('Staking proxy cannot call `withdrawFrom`', async () => {
|
|
const tx = zrxVault.withdrawFrom(staker, new BigNumber(1)).awaitTransactionSuccessAsync({
|
|
from: stakingProxy,
|
|
});
|
|
const expectedError = new StakingRevertErrors.OnlyCallableIfNotInCatastrophicFailureError();
|
|
expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('Staker can withdraw all their ZRX', async () => {
|
|
const receipt = await zrxVault.withdrawAllFrom(staker).awaitTransactionSuccessAsync({
|
|
from: staker,
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Withdrawal,
|
|
staker,
|
|
initialVaultBalance,
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
it('Owner can withdraw ZRX on behalf of a staker', async () => {
|
|
const receipt = await zrxVault.withdrawAllFrom(staker).awaitTransactionSuccessAsync({
|
|
from: owner,
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Withdrawal,
|
|
staker,
|
|
initialVaultBalance,
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
it('Non-owner address can withdraw ZRX on behalf of a staker', async () => {
|
|
const receipt = await zrxVault.withdrawAllFrom(staker).awaitTransactionSuccessAsync({
|
|
from: nonOwnerAddresses[0],
|
|
});
|
|
await verifyTransferPostconditionsAsync(
|
|
ZrxTransfer.Withdrawal,
|
|
staker,
|
|
initialVaultBalance,
|
|
initialVaultBalance,
|
|
initialTokenBalance,
|
|
receipt,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|