Flesh out fillOrder integration tests
This commit is contained in:
parent
8aa69233e0
commit
7aa88307f6
@ -19,7 +19,7 @@ export class BlockchainBalanceStore extends BalanceStore {
|
||||
public constructor(
|
||||
tokenOwnersByName: TokenOwnersByName,
|
||||
tokenContractsByName: Partial<TokenContractsByName>,
|
||||
tokenIds: Partial<TokenIds>,
|
||||
tokenIds: Partial<TokenIds> = {},
|
||||
) {
|
||||
super(tokenOwnersByName, tokenContractsByName);
|
||||
this._tokenContracts = {
|
||||
|
@ -2,4 +2,3 @@ export * from './artifacts';
|
||||
export * from './wrappers';
|
||||
export * from '../test/utils/function_assertions';
|
||||
export * from '../test/utils/deployment_manager';
|
||||
export * from '../test/utils/address_manager';
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { Actor } from './base';
|
||||
import { MakerMixin } from './maker';
|
||||
import { PoolOperatorMixin } from './pool_operator';
|
||||
import { StakerMixin } from './staker';
|
||||
import { KeeperMixin } from './keeper';
|
||||
|
||||
export class OperatorMaker extends PoolOperatorMixin(MakerMixin(Actor)) {}
|
||||
export class StakerMaker extends StakerMixin(MakerMixin(Actor)) {}
|
||||
export class StakerOperator extends StakerMixin(PoolOperatorMixin(Actor)) {}
|
||||
export class OperatorStakerMaker extends PoolOperatorMixin(StakerMixin(MakerMixin(Actor))) {}
|
||||
export class StakerKeeper extends StakerMixin(KeeperMixin(Actor)) {}
|
||||
|
@ -2,5 +2,8 @@ export { Actor } from './base';
|
||||
export { Maker } from './maker';
|
||||
export { PoolOperator } from './pool_operator';
|
||||
export { FeeRecipient } from './fee_recipient';
|
||||
export { Staker } from './staker';
|
||||
export { Keeper } from './keeper';
|
||||
export { Taker } from './taker';
|
||||
export * from './hybrids';
|
||||
export * from './utils';
|
||||
|
72
contracts/integrations/test/actors/keeper.ts
Normal file
72
contracts/integrations/test/actors/keeper.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs, TestStakingEvents } from '@0x/contracts-staking';
|
||||
import { filterLogsToArguments, web3Wrapper } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { BlockParamLiteral, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
export function KeeperMixin<TBase extends Constructor>(Base: TBase) {
|
||||
return class extends Base {
|
||||
public readonly actor: Actor;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `Actor` parameter (assuming `Actor` is used as the base
|
||||
* class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the current epoch, fast-forwarding to the end of the epoch by default.
|
||||
*/
|
||||
public async endEpochAsync(shouldFastForward: boolean = true): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
if (shouldFastForward) {
|
||||
// increase timestamp of next block by how many seconds we need to
|
||||
// get to the next epoch.
|
||||
const epochEndTime = await stakingWrapper.getCurrentEpochEarliestEndTimeInSeconds.callAsync();
|
||||
const lastBlockTime = await web3Wrapper.getBlockTimestampAsync('latest');
|
||||
const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber());
|
||||
await web3Wrapper.increaseTimeAsync(dt);
|
||||
// mine next block
|
||||
await web3Wrapper.mineBlockAsync();
|
||||
}
|
||||
return stakingWrapper.endEpoch.awaitTransactionSuccessAsync({ from: this.actor.address });
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes staking pools corresponding to the given `poolIds`. If none are provided,
|
||||
* finalizes all pools that earned rewards in the previous epoch.
|
||||
*/
|
||||
public async finalizePoolsAsync(poolIds: string[] = []): Promise<TransactionReceiptWithDecodedLogs[]> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
// If no poolIds provided, finalize all active pools from the previous epoch
|
||||
if (poolIds.length === 0) {
|
||||
const previousEpoch = (await stakingWrapper.currentEpoch.callAsync()).minus(1);
|
||||
const events = filterLogsToArguments<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
|
||||
await stakingWrapper.getLogsAsync(
|
||||
TestStakingEvents.StakingPoolEarnedRewardsInEpoch,
|
||||
{ fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest },
|
||||
{ epoch: new BigNumber(previousEpoch) },
|
||||
),
|
||||
TestStakingEvents.StakingPoolEarnedRewardsInEpoch,
|
||||
);
|
||||
poolIds.concat(events.map(event => event.poolId));
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
poolIds.map(
|
||||
async poolId =>
|
||||
await stakingWrapper.finalizePool.awaitTransactionSuccessAsync(poolId, {
|
||||
from: this.actor.address,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Keeper extends KeeperMixin(Actor) {}
|
@ -10,7 +10,7 @@ export interface MakerConfig extends ActorConfig {
|
||||
|
||||
export function MakerMixin<TBase extends Constructor>(Base: TBase) {
|
||||
return class extends Base {
|
||||
public poolId?: string;
|
||||
public makerPoolId?: string;
|
||||
public readonly actor: Actor;
|
||||
public readonly orderFactory: OrderFactory;
|
||||
|
||||
@ -56,7 +56,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase) {
|
||||
*/
|
||||
public async joinStakingPoolAsync(poolId: string): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const stakingContract = this.actor.deployment.staking.stakingWrapper;
|
||||
this.poolId = poolId;
|
||||
this.makerPoolId = poolId;
|
||||
return stakingContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, {
|
||||
from: this.actor.address,
|
||||
});
|
||||
|
@ -1,28 +1,24 @@
|
||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
|
||||
import { Actor, ActorConfig, Constructor } from './base';
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
export interface PoolOperatorConfig extends ActorConfig {
|
||||
operatorShare: number;
|
||||
export interface OperatorShareByPoolId {
|
||||
[poolId: string]: number;
|
||||
}
|
||||
|
||||
export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase) {
|
||||
return class extends Base {
|
||||
public operatorShare: number;
|
||||
public readonly poolIds: string[] = [];
|
||||
public readonly operatorShares: OperatorShareByPoolId = {};
|
||||
public readonly actor: Actor;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `PoolOperatorConfig` parameter (assuming `Actor` is used as the
|
||||
* really expects a single `ActorConfig` parameter (assuming `Actor` is used as the
|
||||
* base class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
|
||||
const { operatorShare } = args[0] as PoolOperatorConfig;
|
||||
this.operatorShare = operatorShare;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,7 +37,7 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase) {
|
||||
|
||||
const createStakingPoolLog = txReceipt.logs[0];
|
||||
const poolId = (createStakingPoolLog as any).args.poolId;
|
||||
this.poolIds.push(poolId);
|
||||
this.operatorShares[poolId] = operatorShare;
|
||||
return poolId;
|
||||
}
|
||||
|
||||
@ -53,7 +49,7 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase) {
|
||||
newOperatorShare: number,
|
||||
): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const stakingContract = this.actor.deployment.staking.stakingWrapper;
|
||||
this.operatorShare = newOperatorShare;
|
||||
this.operatorShares[poolId] = newOperatorShare;
|
||||
return stakingContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
|
||||
poolId,
|
||||
newOperatorShare,
|
||||
|
41
contracts/integrations/test/actors/staker.ts
Normal file
41
contracts/integrations/test/actors/staker.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { StakeInfo, StakeStatus } from '@0x/contracts-staking';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
|
||||
export function StakerMixin<TBase extends Constructor>(Base: TBase) {
|
||||
return class extends Base {
|
||||
public readonly actor: Actor;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `ActorConfig` parameter (assuming `Actor` is used as the base
|
||||
* class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stakes the given amount of ZRX. If `poolId` is provided, subsequently delegates the newly
|
||||
* staked ZRX with that pool.
|
||||
*/
|
||||
public async stakeAsync(amount: BigNumber, poolId?: string): Promise<void> {
|
||||
const { stakingWrapper } = this.actor.deployment.staking;
|
||||
await stakingWrapper.stake.awaitTransactionSuccessAsync(amount, {
|
||||
from: this.actor.address,
|
||||
});
|
||||
if (poolId !== undefined) {
|
||||
await stakingWrapper.moveStake.awaitTransactionSuccessAsync(
|
||||
new StakeInfo(StakeStatus.Undelegated),
|
||||
new StakeInfo(StakeStatus.Delegated, poolId),
|
||||
amount,
|
||||
{ from: this.actor.address },
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Staker extends StakerMixin(Actor) {}
|
45
contracts/integrations/test/actors/taker.ts
Normal file
45
contracts/integrations/test/actors/taker.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
|
||||
|
||||
import { Actor, Constructor } from './base';
|
||||
import { DeploymentManager } from '../utils/deployment_manager';
|
||||
|
||||
export function TakerMixin<TBase extends Constructor>(Base: TBase) {
|
||||
return class extends Base {
|
||||
public readonly actor: Actor;
|
||||
|
||||
/**
|
||||
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
|
||||
* really expects a single `ActorConfig` parameter (assuming `Actor` is used as the base
|
||||
* class).
|
||||
*/
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
this.actor = (this as any) as Actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an order by the given `fillAmount`. Defaults to paying the protocol fee in ETH.
|
||||
*/
|
||||
public async fillOrderAsync(
|
||||
order: SignedOrder,
|
||||
fillAmount: BigNumber,
|
||||
txData: Partial<TxData> = {},
|
||||
): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
return this.actor.deployment.exchange.fillOrder.awaitTransactionSuccessAsync(
|
||||
order,
|
||||
fillAmount,
|
||||
order.signature,
|
||||
{
|
||||
from: this.actor.address,
|
||||
gasPrice: DeploymentManager.gasPrice,
|
||||
value: DeploymentManager.protocolFee,
|
||||
...txData,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class Taker extends TakerMixin(Actor) {}
|
@ -1,135 +0,0 @@
|
||||
import { blockchainTests, constants, expect, filterLogsToArguments, OrderFactory } from '@0x/contracts-test-utils';
|
||||
import { DummyERC20TokenContract, IERC20TokenEvents, IERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
|
||||
import { IExchangeEvents, IExchangeFillEventArgs } from '@0x/contracts-exchange';
|
||||
import { IStakingEventsEvents } from '@0x/contracts-staking';
|
||||
import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
|
||||
import { AddressManager } from '../utils/address_manager';
|
||||
import { DeploymentManager } from '../utils/deployment_manager';
|
||||
|
||||
blockchainTests('Exchange & Staking', env => {
|
||||
let accounts: string[];
|
||||
let makerAddress: string;
|
||||
let takers: string[] = [];
|
||||
let delegators: string[] = [];
|
||||
let feeRecipientAddress: string;
|
||||
let addressManager: AddressManager;
|
||||
let deploymentManager: DeploymentManager;
|
||||
let orderFactory: OrderFactory;
|
||||
let makerAsset: DummyERC20TokenContract;
|
||||
let takerAsset: DummyERC20TokenContract;
|
||||
let feeAsset: DummyERC20TokenContract;
|
||||
|
||||
const GAS_PRICE = 1e9;
|
||||
|
||||
before(async () => {
|
||||
const chainId = await env.getChainIdAsync();
|
||||
accounts = await env.getAccountAddressesAsync();
|
||||
[makerAddress, feeRecipientAddress, takers[0], takers[1], ...delegators] = accounts.slice(1);
|
||||
deploymentManager = await DeploymentManager.deployAsync(env);
|
||||
|
||||
// Create a staking pool with the operator as a maker address.
|
||||
await deploymentManager.staking.stakingWrapper.createStakingPool.awaitTransactionSuccessAsync(
|
||||
constants.ZERO_AMOUNT,
|
||||
true,
|
||||
{ from: makerAddress },
|
||||
);
|
||||
|
||||
// Set up an address for market making.
|
||||
addressManager = new AddressManager();
|
||||
await addressManager.addMakerAsync(
|
||||
deploymentManager,
|
||||
{
|
||||
address: makerAddress,
|
||||
mainToken: deploymentManager.tokens.erc20[0],
|
||||
feeToken: deploymentManager.tokens.erc20[2],
|
||||
},
|
||||
env,
|
||||
deploymentManager.tokens.erc20[1],
|
||||
feeRecipientAddress,
|
||||
chainId,
|
||||
);
|
||||
|
||||
// Set up two addresses for taking orders.
|
||||
await Promise.all(
|
||||
takers.map(taker =>
|
||||
addressManager.addTakerAsync(deploymentManager, {
|
||||
address: taker,
|
||||
mainToken: deploymentManager.tokens.erc20[1],
|
||||
feeToken: deploymentManager.tokens.erc20[2],
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe('fillOrder', () => {
|
||||
it('should be able to fill an order', async () => {
|
||||
const order = await addressManager.makers[0].orderFactory.newSignedOrderAsync({
|
||||
makerAddress,
|
||||
makerAssetAmount: new BigNumber(1),
|
||||
takerAssetAmount: new BigNumber(1),
|
||||
makerFee: constants.ZERO_AMOUNT,
|
||||
takerFee: constants.ZERO_AMOUNT,
|
||||
feeRecipientAddress,
|
||||
});
|
||||
|
||||
const receipt = await deploymentManager.exchange.fillOrder.awaitTransactionSuccessAsync(
|
||||
order,
|
||||
new BigNumber(1),
|
||||
order.signature,
|
||||
{
|
||||
from: takers[0],
|
||||
gasPrice: GAS_PRICE,
|
||||
value: DeploymentManager.protocolFeeMultiplier.times(GAS_PRICE),
|
||||
},
|
||||
);
|
||||
|
||||
// Ensure that the number of emitted logs is equal to 3. There should have been a fill event
|
||||
// and two transfer events. A 'StakingPoolActivated' event should not be expected because
|
||||
// the only staking pool that was created does not have enough stake.
|
||||
expect(receipt.logs.length).to.be.eq(3);
|
||||
|
||||
// Ensure that the fill event was correct.
|
||||
const fillArgs = filterLogsToArguments<IExchangeFillEventArgs>(receipt.logs, IExchangeEvents.Fill);
|
||||
expect(fillArgs.length).to.be.eq(1);
|
||||
expect(fillArgs).to.be.deep.eq([
|
||||
{
|
||||
makerAddress,
|
||||
feeRecipientAddress,
|
||||
makerAssetData: order.makerAssetData,
|
||||
takerAssetData: order.takerAssetData,
|
||||
makerFeeAssetData: order.makerFeeAssetData,
|
||||
takerFeeAssetData: order.takerFeeAssetData,
|
||||
orderHash: orderHashUtils.getOrderHashHex(order),
|
||||
takerAddress: takers[0],
|
||||
senderAddress: takers[0],
|
||||
makerAssetFilledAmount: order.makerAssetAmount,
|
||||
takerAssetFilledAmount: order.takerAssetAmount,
|
||||
makerFeePaid: constants.ZERO_AMOUNT,
|
||||
takerFeePaid: constants.ZERO_AMOUNT,
|
||||
protocolFeePaid: DeploymentManager.protocolFeeMultiplier.times(GAS_PRICE),
|
||||
},
|
||||
]);
|
||||
|
||||
// Ensure that the transfer events were correctly emitted.
|
||||
const transferArgs = filterLogsToArguments<IERC20TokenTransferEventArgs>(
|
||||
receipt.logs,
|
||||
IERC20TokenEvents.Transfer,
|
||||
);
|
||||
expect(transferArgs.length).to.be.eq(2);
|
||||
expect(transferArgs).to.be.deep.eq([
|
||||
{
|
||||
_from: takers[0],
|
||||
_to: makerAddress,
|
||||
_value: order.takerAssetAmount,
|
||||
},
|
||||
{
|
||||
_from: makerAddress,
|
||||
_to: takers[0],
|
||||
_value: order.makerAssetAmount,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,331 @@
|
||||
import { blockchainTests, constants, expect } from '@0x/contracts-test-utils';
|
||||
import { IERC20TokenEvents, IERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
|
||||
import {
|
||||
BlockchainBalanceStore,
|
||||
IExchangeEvents,
|
||||
IExchangeFillEventArgs,
|
||||
LocalBalanceStore,
|
||||
} from '@0x/contracts-exchange';
|
||||
import {
|
||||
constants as stakingConstants,
|
||||
IStakingEventsEpochEndedEventArgs,
|
||||
IStakingEventsEpochFinalizedEventArgs,
|
||||
IStakingEventsEvents,
|
||||
IStakingEventsRewardsPaidEventArgs,
|
||||
IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs,
|
||||
} from '@0x/contracts-staking';
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { assetDataUtils, orderHashUtils } from '@0x/order-utils';
|
||||
import { toBaseUnitAmount, verifyEvents } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
|
||||
|
||||
import { Taker, actorAddressesByName, FeeRecipient, Maker, OperatorStakerMaker, StakerKeeper } from '../actors';
|
||||
import { DeploymentManager } from '../utils/deployment_manager';
|
||||
|
||||
blockchainTests.resets('fillOrder integration tests', env => {
|
||||
let deployment: DeploymentManager;
|
||||
let balanceStore: BlockchainBalanceStore;
|
||||
|
||||
let feeRecipient: FeeRecipient;
|
||||
let operator: OperatorStakerMaker;
|
||||
let maker: Maker;
|
||||
let taker: Taker;
|
||||
let delegator: StakerKeeper;
|
||||
|
||||
let poolId: string;
|
||||
|
||||
before(async () => {
|
||||
deployment = await DeploymentManager.deployAsync(env, {
|
||||
numErc20TokensToDeploy: 2,
|
||||
numErc721TokensToDeploy: 0,
|
||||
numErc1155TokensToDeploy: 0,
|
||||
});
|
||||
const [makerToken, takerToken] = deployment.tokens.erc20;
|
||||
|
||||
feeRecipient = new FeeRecipient({
|
||||
name: 'Fee recipient',
|
||||
deployment,
|
||||
});
|
||||
const orderConfig = {
|
||||
feeRecipientAddress: feeRecipient.address,
|
||||
makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address),
|
||||
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address),
|
||||
makerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address),
|
||||
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address),
|
||||
makerFee: constants.ZERO_AMOUNT,
|
||||
takerFee: constants.ZERO_AMOUNT,
|
||||
};
|
||||
operator = new OperatorStakerMaker({
|
||||
name: 'Pool operator',
|
||||
deployment,
|
||||
orderConfig,
|
||||
});
|
||||
maker = new Maker({
|
||||
name: 'Maker',
|
||||
deployment,
|
||||
orderConfig,
|
||||
});
|
||||
taker = new Taker({ name: 'Taker', deployment });
|
||||
delegator = new StakerKeeper({ name: 'Delegator', deployment });
|
||||
|
||||
await operator.configureERC20TokenAsync(makerToken);
|
||||
await maker.configureERC20TokenAsync(makerToken);
|
||||
await taker.configureERC20TokenAsync(takerToken);
|
||||
await taker.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address);
|
||||
|
||||
await operator.configureERC20TokenAsync(deployment.tokens.zrx);
|
||||
await delegator.configureERC20TokenAsync(deployment.tokens.zrx);
|
||||
|
||||
// Create a staking pool with the operator as a maker.
|
||||
poolId = await operator.createStakingPoolAsync(0.95 * stakingConstants.PPM, true);
|
||||
// A vanilla maker joins the pool as well.
|
||||
await maker.joinStakingPoolAsync(poolId);
|
||||
|
||||
const tokenOwners = {
|
||||
...actorAddressesByName([feeRecipient, operator, maker, taker, delegator]),
|
||||
StakingProxy: deployment.staking.stakingProxy.address,
|
||||
ZrxVault: deployment.staking.zrxVault.address,
|
||||
};
|
||||
console.log(tokenOwners);
|
||||
const tokenContracts = {
|
||||
erc20: { makerToken, takerToken, ZRX: deployment.tokens.zrx, WETH: deployment.tokens.weth },
|
||||
};
|
||||
balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts);
|
||||
await balanceStore.updateBalancesAsync();
|
||||
});
|
||||
|
||||
function simulateFill(
|
||||
order: SignedOrder,
|
||||
txReceipt: TransactionReceiptWithDecodedLogs,
|
||||
msgValue: BigNumber = DeploymentManager.protocolFee,
|
||||
): LocalBalanceStore {
|
||||
const localBalanceStore = LocalBalanceStore.create(balanceStore);
|
||||
// Transaction gas cost
|
||||
localBalanceStore.burnGas(txReceipt.from, DeploymentManager.gasPrice.times(txReceipt.gasUsed));
|
||||
|
||||
// Taker -> Maker
|
||||
localBalanceStore.transferAsset(taker.address, maker.address, order.takerAssetAmount, order.takerAssetData);
|
||||
// Maker -> Taker
|
||||
localBalanceStore.transferAsset(maker.address, taker.address, order.makerAssetAmount, order.makerAssetData);
|
||||
|
||||
// Protocol fee
|
||||
if (msgValue.isGreaterThanOrEqualTo(DeploymentManager.protocolFee)) {
|
||||
localBalanceStore.sendEth(
|
||||
txReceipt.from,
|
||||
deployment.staking.stakingProxy.address,
|
||||
DeploymentManager.protocolFee,
|
||||
);
|
||||
msgValue = msgValue.minus(DeploymentManager.protocolFee);
|
||||
} else {
|
||||
localBalanceStore.transferAsset(
|
||||
taker.address,
|
||||
deployment.staking.stakingProxy.address,
|
||||
DeploymentManager.protocolFee,
|
||||
assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address),
|
||||
);
|
||||
}
|
||||
|
||||
return localBalanceStore;
|
||||
}
|
||||
|
||||
function verifyFillEvents(order: SignedOrder, receipt: TransactionReceiptWithDecodedLogs): void {
|
||||
// Ensure that the fill event was correct.
|
||||
verifyEvents<IExchangeFillEventArgs>(
|
||||
receipt,
|
||||
[
|
||||
{
|
||||
makerAddress: maker.address,
|
||||
feeRecipientAddress: feeRecipient.address,
|
||||
makerAssetData: order.makerAssetData,
|
||||
takerAssetData: order.takerAssetData,
|
||||
makerFeeAssetData: order.makerFeeAssetData,
|
||||
takerFeeAssetData: order.takerFeeAssetData,
|
||||
orderHash: orderHashUtils.getOrderHashHex(order),
|
||||
takerAddress: taker.address,
|
||||
senderAddress: taker.address,
|
||||
makerAssetFilledAmount: order.makerAssetAmount,
|
||||
takerAssetFilledAmount: order.takerAssetAmount,
|
||||
makerFeePaid: constants.ZERO_AMOUNT,
|
||||
takerFeePaid: constants.ZERO_AMOUNT,
|
||||
protocolFeePaid: DeploymentManager.protocolFee,
|
||||
},
|
||||
],
|
||||
IExchangeEvents.Fill,
|
||||
);
|
||||
|
||||
// Ensure that the transfer events were correctly emitted.
|
||||
verifyEvents<IERC20TokenTransferEventArgs>(
|
||||
receipt,
|
||||
[
|
||||
{
|
||||
_from: taker.address,
|
||||
_to: maker.address,
|
||||
_value: order.takerAssetAmount,
|
||||
},
|
||||
{
|
||||
_from: maker.address,
|
||||
_to: taker.address,
|
||||
_value: order.makerAssetAmount,
|
||||
},
|
||||
],
|
||||
IERC20TokenEvents.Transfer,
|
||||
);
|
||||
}
|
||||
|
||||
it('should fill an order', async () => {
|
||||
// Create and fill the order
|
||||
const order = await maker.signOrderAsync();
|
||||
const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount);
|
||||
|
||||
// Check balances
|
||||
const expectedBalances = simulateFill(order, receipt);
|
||||
await balanceStore.updateBalancesAsync();
|
||||
balanceStore.assertEquals(expectedBalances);
|
||||
|
||||
// There should have been a fill event and two transfer events. A
|
||||
// 'StakingPoolEarnedRewardsInEpoch' event should not be expected because the only staking
|
||||
// pool that was created does not have enough stake.
|
||||
verifyFillEvents(order, receipt);
|
||||
});
|
||||
it('should activate a staking pool if it has sufficient stake', async () => {
|
||||
// Stake just enough to qualify the pool for rewards.
|
||||
await delegator.stakeAsync(toBaseUnitAmount(100), poolId);
|
||||
|
||||
// The delegator, functioning as a keeper, ends the epoch so that delegated stake (theirs
|
||||
// and the operator's) becomes active. This puts the staking pool above the minimumPoolStake
|
||||
// threshold, so it should be able to earn rewards in the new epoch.
|
||||
// Finalizing the pool shouldn't settle rewards because it didn't earn rewards last epoch.
|
||||
await delegator.endEpochAsync();
|
||||
await delegator.finalizePoolsAsync([poolId]);
|
||||
await balanceStore.updateBalancesAsync();
|
||||
|
||||
// Create and fill the order
|
||||
const order = await maker.signOrderAsync();
|
||||
const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount);
|
||||
|
||||
// Check balances
|
||||
const expectedBalances = simulateFill(order, receipt);
|
||||
await balanceStore.updateBalancesAsync();
|
||||
balanceStore.assertEquals(expectedBalances);
|
||||
|
||||
// In addition to the fill event and two transfer events emitted in the previous test, we
|
||||
// now expect a `StakingPoolEarnedRewardsInEpoch` event to be emitted because the staking
|
||||
// pool now has enough stake in the current epoch to earn rewards.
|
||||
verifyFillEvents(order, receipt);
|
||||
const currentEpoch = await deployment.staking.stakingWrapper.currentEpoch.callAsync();
|
||||
verifyEvents<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
|
||||
receipt,
|
||||
[
|
||||
{
|
||||
epoch: currentEpoch,
|
||||
poolId,
|
||||
},
|
||||
],
|
||||
IStakingEventsEvents.StakingPoolEarnedRewardsInEpoch,
|
||||
);
|
||||
});
|
||||
it('should pay out rewards to operator and delegator', async () => {
|
||||
// Operator and delegator each stake some ZRX; wait an epoch so that the stake is active.
|
||||
await operator.stakeAsync(toBaseUnitAmount(100), poolId);
|
||||
await delegator.stakeAsync(toBaseUnitAmount(50), poolId);
|
||||
await delegator.endEpochAsync();
|
||||
|
||||
// Create and fill the order. One order's worth of protocol fees are now available as rewards.
|
||||
const order = await maker.signOrderAsync();
|
||||
await taker.fillOrderAsync(order, order.takerAssetAmount);
|
||||
const rewardsAvailable = DeploymentManager.protocolFee;
|
||||
|
||||
// Fetch the current balances
|
||||
await balanceStore.updateBalancesAsync();
|
||||
const expectedBalances = LocalBalanceStore.create(balanceStore);
|
||||
|
||||
// End the epoch. This should wrap the staking proxy's ETH balance.
|
||||
const endEpochReceipt = await delegator.endEpochAsync();
|
||||
const newEpoch = await deployment.staking.stakingWrapper.currentEpoch.callAsync();
|
||||
|
||||
// Check balances
|
||||
expectedBalances.wrapEth(
|
||||
deployment.staking.stakingProxy.address,
|
||||
deployment.tokens.weth.address,
|
||||
DeploymentManager.protocolFee,
|
||||
);
|
||||
expectedBalances.burnGas(delegator.address, DeploymentManager.gasPrice.times(endEpochReceipt.gasUsed));
|
||||
await balanceStore.updateBalancesAsync();
|
||||
balanceStore.assertEquals(expectedBalances);
|
||||
|
||||
// Check the EpochEnded event
|
||||
const weightedDelegatorStake = toBaseUnitAmount(50).times(0.9);
|
||||
verifyEvents<IStakingEventsEpochEndedEventArgs>(
|
||||
endEpochReceipt,
|
||||
[
|
||||
{
|
||||
epoch: newEpoch.minus(1),
|
||||
numPoolsToFinalize: new BigNumber(1),
|
||||
rewardsAvailable,
|
||||
totalFeesCollected: DeploymentManager.protocolFee,
|
||||
totalWeightedStake: toBaseUnitAmount(100).plus(weightedDelegatorStake),
|
||||
},
|
||||
],
|
||||
IStakingEventsEvents.EpochEnded,
|
||||
);
|
||||
|
||||
// The rewards are split between the operator and delegator based on the pool's operatorShare
|
||||
const operatorReward = rewardsAvailable
|
||||
.times(operator.operatorShares[poolId])
|
||||
.dividedToIntegerBy(constants.PPM_DENOMINATOR);
|
||||
const delegatorReward = rewardsAvailable.minus(operatorReward);
|
||||
|
||||
// Finalize the pool. This should automatically pay the operator in WETH.
|
||||
const [finalizePoolReceipt] = await delegator.finalizePoolsAsync([poolId]);
|
||||
|
||||
// Check balances
|
||||
expectedBalances.transferAsset(
|
||||
deployment.staking.stakingProxy.address,
|
||||
operator.address,
|
||||
operatorReward,
|
||||
assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address),
|
||||
);
|
||||
expectedBalances.burnGas(delegator.address, DeploymentManager.gasPrice.times(finalizePoolReceipt.gasUsed));
|
||||
await balanceStore.updateBalancesAsync();
|
||||
balanceStore.assertEquals(expectedBalances);
|
||||
|
||||
// Check finalization events
|
||||
verifyEvents<IStakingEventsRewardsPaidEventArgs>(
|
||||
finalizePoolReceipt,
|
||||
[
|
||||
{
|
||||
epoch: newEpoch,
|
||||
poolId,
|
||||
operatorReward,
|
||||
membersReward: delegatorReward,
|
||||
},
|
||||
],
|
||||
IStakingEventsEvents.RewardsPaid,
|
||||
);
|
||||
verifyEvents<IStakingEventsEpochFinalizedEventArgs>(
|
||||
finalizePoolReceipt,
|
||||
[
|
||||
{
|
||||
epoch: newEpoch.minus(1),
|
||||
rewardsPaid: rewardsAvailable,
|
||||
rewardsRemaining: constants.ZERO_AMOUNT,
|
||||
},
|
||||
],
|
||||
IStakingEventsEvents.EpochFinalized,
|
||||
);
|
||||
});
|
||||
it('should credit rewards from orders made by the operator to their pool', async () => {
|
||||
// Stake just enough to qualify the pool for rewards.
|
||||
await delegator.stakeAsync(toBaseUnitAmount(100), poolId);
|
||||
await delegator.endEpochAsync();
|
||||
|
||||
// Create and fill the order
|
||||
const order = await operator.signOrderAsync();
|
||||
await taker.fillOrderAsync(order, order.takerAssetAmount);
|
||||
|
||||
// Check that the pool has collected fees from the above fill.
|
||||
const poolStats = await deployment.staking.stakingWrapper.getStakingPoolStatsThisEpoch.callAsync(poolId);
|
||||
expect(poolStats.feesCollected).to.bignumber.equal(DeploymentManager.protocolFee);
|
||||
});
|
||||
});
|
@ -1,97 +0,0 @@
|
||||
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
|
||||
import { constants, OrderFactory, BlockchainTestsEnvironment } from '@0x/contracts-test-utils';
|
||||
import { assetDataUtils, Order, SignatureType, SignedOrder } from '@0x/order-utils';
|
||||
|
||||
import { DeploymentManager } from '../../src';
|
||||
|
||||
interface MarketMaker {
|
||||
address: string;
|
||||
orderFactory: OrderFactory;
|
||||
}
|
||||
|
||||
interface ConfigurationArgs {
|
||||
address: string;
|
||||
mainToken: DummyERC20TokenContract;
|
||||
feeToken: DummyERC20TokenContract;
|
||||
}
|
||||
|
||||
export class AddressManager {
|
||||
// A set of addresses that have been configured for market making.
|
||||
public makers: MarketMaker[];
|
||||
|
||||
// A set of addresses that have been configured to take orders.
|
||||
public takers: string[];
|
||||
|
||||
/**
|
||||
* Sets up an address to take orders.
|
||||
*/
|
||||
public async addTakerAsync(deploymentManager: DeploymentManager, configArgs: ConfigurationArgs): Promise<void> {
|
||||
// Configure the taker address with the taker and fee tokens.
|
||||
await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.mainToken);
|
||||
await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.feeToken);
|
||||
|
||||
// Add the taker to the list of configured taker addresses.
|
||||
this.takers.push(configArgs.address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up an address for market making.
|
||||
*/
|
||||
public async addMakerAsync(
|
||||
deploymentManager: DeploymentManager,
|
||||
configArgs: ConfigurationArgs,
|
||||
environment: BlockchainTestsEnvironment,
|
||||
takerToken: DummyERC20TokenContract,
|
||||
feeRecipientAddress: string,
|
||||
chainId: number,
|
||||
): Promise<void> {
|
||||
const accounts = await environment.getAccountAddressesAsync();
|
||||
|
||||
// Set up order signing for the maker address.
|
||||
const defaultOrderParams = {
|
||||
...constants.STATIC_ORDER_PARAMS,
|
||||
makerAddress: configArgs.address,
|
||||
makerAssetData: assetDataUtils.encodeERC20AssetData(configArgs.mainToken.address),
|
||||
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address),
|
||||
makerFeeAssetData: assetDataUtils.encodeERC20AssetData(configArgs.feeToken.address),
|
||||
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(configArgs.feeToken.address),
|
||||
feeRecipientAddress,
|
||||
exchangeAddress: deploymentManager.exchange.address,
|
||||
chainId,
|
||||
};
|
||||
const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(configArgs.address)];
|
||||
const orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
||||
|
||||
// Configure the maker address with the maker and fee tokens.
|
||||
await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.mainToken);
|
||||
await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.feeToken);
|
||||
|
||||
// Add the maker to the list of configured maker addresses.
|
||||
this.makers.push({
|
||||
address: configArgs.address,
|
||||
orderFactory,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up initial account balances for a token and approves the ERC20 asset proxy
|
||||
* to transfer the token.
|
||||
*/
|
||||
protected async _configureTokenForAddressAsync(
|
||||
deploymentManager: DeploymentManager,
|
||||
address: string,
|
||||
token: DummyERC20TokenContract,
|
||||
): Promise<void> {
|
||||
await token.setBalance.awaitTransactionSuccessAsync(address, constants.INITIAL_ERC20_BALANCE);
|
||||
await token.approve.awaitTransactionSuccessAsync(
|
||||
deploymentManager.assetProxies.erc20Proxy.address,
|
||||
constants.MAX_UINT256,
|
||||
{ from: address },
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.makers = [];
|
||||
this.takers = [];
|
||||
}
|
||||
}
|
@ -7,12 +7,7 @@ import {
|
||||
StaticCallProxyContract,
|
||||
} from '@0x/contracts-asset-proxy';
|
||||
import { artifacts as ERC1155Artifacts, ERC1155MintableContract } from '@0x/contracts-erc1155';
|
||||
import {
|
||||
DummyERC20TokenContract,
|
||||
artifacts as ERC20Artifacts,
|
||||
ZRXTokenContract,
|
||||
WETH9Contract,
|
||||
} from '@0x/contracts-erc20';
|
||||
import { DummyERC20TokenContract, artifacts as ERC20Artifacts, WETH9Contract } from '@0x/contracts-erc20';
|
||||
import { artifacts as ERC721Artifacts, DummyERC721TokenContract } from '@0x/contracts-erc721';
|
||||
import {
|
||||
artifacts as exchangeArtifacts,
|
||||
@ -108,7 +103,7 @@ interface TokenContracts {
|
||||
erc721: DummyERC721TokenContract[];
|
||||
erc1155: ERC1155MintableContract[];
|
||||
weth: WETH9Contract;
|
||||
zrx: ZRXTokenContract;
|
||||
zrx: DummyERC20TokenContract;
|
||||
}
|
||||
|
||||
// Options to be passed to `deployAsync`
|
||||
@ -150,7 +145,7 @@ export class DeploymentManager {
|
||||
exchangeArtifacts.Exchange,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
{ ...ERC20Artifacts, ...exchangeArtifacts },
|
||||
{ ...ERC20Artifacts, ...exchangeArtifacts, ...stakingArtifacts },
|
||||
new BigNumber(chainId),
|
||||
);
|
||||
const governor = await ZeroExGovernorContract.deployFrom0xArtifactAsync(
|
||||
@ -357,7 +352,7 @@ export class DeploymentManager {
|
||||
txDefaults,
|
||||
stakingArtifacts,
|
||||
tokens.weth.address,
|
||||
tokens.zrx.address,
|
||||
zrxVault.address,
|
||||
);
|
||||
const stakingProxy = await StakingProxyContract.deployFrom0xArtifactAsync(
|
||||
stakingArtifacts.StakingProxy,
|
||||
@ -366,7 +361,17 @@ export class DeploymentManager {
|
||||
stakingArtifacts,
|
||||
stakingLogic.address,
|
||||
);
|
||||
const stakingWrapper = new TestStakingContract(stakingProxy.address, environment.provider);
|
||||
|
||||
const logDecoderDependencies = _.mapValues(
|
||||
{ ...stakingArtifacts, ...ERC20Artifacts, ...exchangeArtifacts },
|
||||
v => v.compilerOutput.abi,
|
||||
);
|
||||
const stakingWrapper = new TestStakingContract(
|
||||
stakingProxy.address,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
logDecoderDependencies,
|
||||
);
|
||||
|
||||
// Add the zrx vault and the weth contract to the staking proxy.
|
||||
await stakingWrapper.setWethContract.awaitTransactionSuccessAsync(tokens.weth.address, { from: owner });
|
||||
@ -376,9 +381,13 @@ export class DeploymentManager {
|
||||
await stakingProxy.addAuthorizedAddress.awaitTransactionSuccessAsync(owner, { from: owner });
|
||||
await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(owner, { from: owner });
|
||||
|
||||
// Authorize the zrx vault in the erc20 proxy
|
||||
await assetProxies.erc20Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync(zrxVault.address, {
|
||||
from: owner,
|
||||
});
|
||||
|
||||
// Configure the zrx vault and the staking contract.
|
||||
await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(stakingProxy.address, { from: owner });
|
||||
await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(stakingProxy.address, { from: owner });
|
||||
|
||||
return {
|
||||
stakingLogic,
|
||||
@ -461,11 +470,15 @@ export class DeploymentManager {
|
||||
txDefaults,
|
||||
ERC20Artifacts,
|
||||
);
|
||||
const zrx = await ZRXTokenContract.deployFrom0xArtifactAsync(
|
||||
ERC20Artifacts.ZRXToken,
|
||||
const zrx = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
|
||||
ERC20Artifacts.DummyERC20Token,
|
||||
environment.provider,
|
||||
txDefaults,
|
||||
ERC20Artifacts,
|
||||
constants.DUMMY_TOKEN_NAME,
|
||||
constants.DUMMY_TOKEN_SYMBOL,
|
||||
constants.DUMMY_TOKEN_DECIMALS,
|
||||
constants.DUMMY_TOKEN_TOTAL_SUPPLY,
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -82,7 +82,6 @@ contract MixinExchangeFees is
|
||||
}
|
||||
|
||||
// Look up the pool stats and aggregated stats for this epoch.
|
||||
|
||||
uint256 currentEpoch_ = currentEpoch;
|
||||
IStructs.PoolStats storage poolStatsPtr = poolStatsByEpoch[poolId][currentEpoch_];
|
||||
IStructs.AggregatedStats storage aggregatedStatsPtr = aggregatedStatsByEpoch[currentEpoch_];
|
||||
|
@ -37,7 +37,7 @@
|
||||
},
|
||||
"config": {
|
||||
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
|
||||
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeStorage|TestMixinStakingPool|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault).json"
|
||||
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault).json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './wrappers';
|
||||
export * from './artifacts';
|
||||
export { constants } from '../test/utils/constants';
|
||||
export * from '../test/utils/types';
|
||||
|
@ -38,8 +38,8 @@ export function verifyEventsFromLogs<TEventArgs>(
|
||||
eventName: string,
|
||||
): void {
|
||||
const _logs = filterLogsToArguments<TEventArgs>(logs, eventName);
|
||||
expect(_logs.length).to.eq(expectedEvents.length);
|
||||
expect(_logs.length, `Number of ${eventName} events emitted`).to.eq(expectedEvents.length);
|
||||
_logs.forEach((log, index) => {
|
||||
expect(log).to.deep.equal(expectedEvents[index]);
|
||||
expect(log, `${eventName} event ${index}`).to.deep.equal(expectedEvents[index]);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user