@0x/contracts-staking: Replace MixinDeploymentConstants with MixinHyperParameters.

`@0x/contracts-staking`: Add `init()` to `Staking` contract.
`@0x/contracts-staking`: Add `_initMixinScheduler()` to `MixinScheduler`.
`@0x/contracts-staking`: Automaticallly call `Staking.init()` in `StakingProxy.attachStakingContract()`.
`@0x/contracts-staking`: Remove `setCobbDouglasAlpha()` in favor of `tune()`.
`@0x/contracts-staking`: Exclude pools with `stake < minimumPoolStake` in `payProtocolFee()`.
This commit is contained in:
Lawrence Forman
2019-09-06 12:44:11 -04:00
parent 70db4d8847
commit 6488f91e6e
15 changed files with 233 additions and 108 deletions

View File

@@ -34,10 +34,10 @@ import "./staking_pools/MixinStakingPoolRewards.sol";
contract Staking is
IStaking,
IStakingEvents,
MixinDeploymentConstants,
Ownable,
MixinConstants,
MixinStorage,
MixinHyperParameters,
MixinZrxVault,
MixinExchangeManager,
MixinScheduler,
@@ -49,6 +49,18 @@ contract Staking is
MixinStakingPool,
MixinExchangeFees
{
/// @dev Initialize storage owned by this contract.
/// This function should not be called directly.
/// The StakingProxy contract will call it in `attachStakingContract()`.
function init()
external
onlyOwner
{
// DANGER! When performing upgrades, take care to modify this logic
// not to accidentally overwrite existing state.
MixinStorage._initMixinScheduler();
}
// this contract can receive ETH
// solhint-disable no-empty-blocks
function ()

View File

@@ -25,7 +25,7 @@ import "./interfaces/IStakingProxy.sol";
contract StakingProxy is
IStakingProxy,
MixinDeploymentConstants,
MixinHyperParameters,
MixinConstants,
MixinStorage
{
@@ -66,6 +66,7 @@ contract StakingProxy is
{
stakingContract = _stakingContract;
readOnlyProxyCallee = _stakingContract;
stakingContract.init.delegatecall();
emit StakingContractAttachedToProxy(_stakingContract);
}

View File

@@ -59,28 +59,6 @@ contract MixinExchangeFees is
{
using LibSafeMath for uint256;
/// @dev Set the cobb douglas alpha value used when calculating rewards.
/// Valid inputs: 0 <= `numerator` / `denominator` <= 1.0
/// @param numerator The alpha numerator.
/// @param denominator The alpha denominator.
function setCobbDouglasAlpha(
uint256 numerator,
uint256 denominator
)
external
onlyOwner
{
if (int256(numerator) < 0 || int256(denominator) <= 0 || numerator > denominator) {
LibRichErrors.rrevert(LibStakingRichErrors.InvalidCobbDouglasAlphaError(
numerator,
denominator
));
}
cobbDouglasAlphaNumerator = numerator;
cobbDouglasAlphaDenomintor = denominator;
emit CobbDouglasAlphaChanged(numerator, denominator);
}
/// @dev Pays a protocol fee in ETH or WETH.
/// Only a known 0x exchange can call this method. See (MixinExchangeManager).
/// @param makerAddress The address of the order's maker.
@@ -126,15 +104,15 @@ contract MixinExchangeFees is
// Only attribute the protocol fee payment to a pool if the maker is registered to a pool.
if (poolId != NIL_POOL_ID) {
// Use the maker pool id to get the amount of fees collected during this epoch in the pool.
uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId];
// Update the amount of protocol fees paid to this pool this epoch.
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(protocolFeePaid);
// If there were no fees collected prior to this payment, activate the pool that is being paid.
if (_feesCollectedThisEpoch == 0) {
activePoolsThisEpoch.push(poolId);
uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance;
// Ignore pools with dust stake.
if (poolStake >= minimumPoolStake) {
// Credit the pool.
uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId];
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(amount);
if (_feesCollectedThisEpoch == 0) {
activePoolsThisEpoch.push(poolId);
}
}
}
}
@@ -249,6 +227,7 @@ contract MixinExchangeFees is
// step 2/4 - compute stats for active maker pools
IStructs.ActivePool[] memory activePools = new IStructs.ActivePool[](totalActivePools);
uint32 delegatedStakeWeight = rewardDelegatedStakeWeight;
for (uint256 i = 0; i != totalActivePools; i++) {
bytes32 poolId = activePoolsThisEpoch[i];
@@ -258,7 +237,7 @@ contract MixinExchangeFees is
uint256 weightedStake = stakeHeldByPoolOperator.safeAdd(
totalStakeDelegatedToPool
.safeSub(stakeHeldByPoolOperator)
.safeMul(REWARD_DELEGATED_STAKE_WEIGHT)
.safeMul(delegatedStakeWeight)
.safeDiv(PPM_DENOMINATOR)
);

View File

@@ -31,7 +31,6 @@ import "../immutable/MixinStorage.sol";
/// then it should be removed.
contract MixinExchangeManager is
IStakingEvents,
MixinDeploymentConstants,
MixinConstants,
MixinStorage
{

View File

@@ -18,13 +18,11 @@
pragma solidity ^0.5.9;
import "./MixinDeploymentConstants.sol";
contract MixinConstants is
MixinDeploymentConstants
contract MixinConstants
{
uint32 constant internal PPM_DENOMINATOR = 1000000;
// 100% in parts-per-million.
uint32 constant internal PPM_DENOMINATOR = 10**6;
// The upper 16 bytes represent the pool id, so this would be pool id 1. See MixinStakinPool for more information.
bytes32 constant internal INITIAL_POOL_ID = 0x0000000000000000000000000000000100000000000000000000000000000000;
@@ -43,4 +41,7 @@ contract MixinConstants is
uint64 constant internal INITIAL_TIMELOCK_PERIOD = INITIAL_EPOCH;
uint256 constant internal MIN_TOKEN_VALUE = 10**18;
// TODO(dorothy-zbornak): Remove when signatures are removed from maker handshake.
uint256 constant internal CHAIN_ID = 1;
}

View File

@@ -0,0 +1,96 @@
/*
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;
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/Ownable.sol";
import "../interfaces/IStakingEvents.sol";
import "../libs/LibStakingRichErrors.sol";
import "./MixinConstants.sol";
contract MixinHyperParameters is
IStakingEvents,
Ownable,
MixinConstants
{
// Minimum seconds between epochs.
uint256 internal epochDurationInSeconds = 2 weeks;
// How much delegated stake is weighted vs operator stake, in ppm.
uint32 internal rewardDelegatedStakeWeight = 0.9 * PPM_DENOMINATOR; // 90%
// Minimum amount of stake required in a pool to collect rewards.
uint256 internal minimumPoolStake = 100 * MIN_TOKEN_VALUE; // 100 ZRX
// Numerator for cobb douglas alpha factor.
uint256 internal cobbDouglasAlphaNumerator = 1;
// Denominator for cobb douglas alpha factor.
uint256 internal cobbDouglasAlphaDenomintor = 2;
/// @dev Set all hyperparameters at once.
/// @param _epochDurationInSeconds Minimum seconds between epochs.
/// @param _rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm.
/// @param _minimumPoolStake Minimum amount of stake required in a pool to collect rewards.
/// @param _cobbDouglasAlphaNumerator Numerator for cobb douglas alpha factor.
/// @param _cobbDouglasAlphaDenomintor Denominator for cobb douglas alpha factor.
function tune(
uint256 _epochDurationInSeconds,
uint32 _rewardDelegatedStakeWeight,
uint256 _minimumPoolStake,
uint256 _cobbDouglasAlphaNumerator,
uint256 _cobbDouglasAlphaDenomintor
)
external
onlyOwner
{
_assertValidCobbDouglasAlpha(
_cobbDouglasAlphaNumerator,
_cobbDouglasAlphaDenomintor
);
epochDurationInSeconds = _epochDurationInSeconds;
rewardDelegatedStakeWeight = _rewardDelegatedStakeWeight;
minimumPoolStake = _minimumPoolStake;
cobbDouglasAlphaNumerator = _cobbDouglasAlphaNumerator;
cobbDouglasAlphaDenomintor = _cobbDouglasAlphaDenomintor;
emit Tuned(
epochDurationInSeconds,
rewardDelegatedStakeWeight,
minimumPoolStake,
cobbDouglasAlphaNumerator,
cobbDouglasAlphaDenomintor
);
}
/// @dev Asserts that cobb douglas alpha values are valid.
function _assertValidCobbDouglasAlpha(
uint256 numerator,
uint256 denominator
)
private
{
if (int256(numerator) < 0
|| int256(denominator) <= 0
|| numerator > denominator
) {
LibRichErrors.rrevert(
LibStakingRichErrors.InvalidTuningValue(
LibStakingRichErrors.InvalidTuningValueErrorCode.InvalidCobbDouglasAlpha
));
}
}
}

View File

@@ -33,7 +33,6 @@ import "../libs/LibStakingRichErrors.sol";
// solhint-disable max-states-count, no-empty-blocks
contract MixinStorage is
MixinDeploymentConstants,
Ownable,
MixinConstants
{
@@ -126,10 +125,4 @@ contract MixinStorage is
// Rebate Vault (stores rewards for pools before they are moved to the eth vault on a per-user basis)
IStakingPoolRewardVault internal rewardVault;
// Numerator for cobb douglas alpha factor.
uint256 internal cobbDouglasAlphaNumerator = 1;
// Denominator for cobb douglas alpha factor.
uint256 internal cobbDouglasAlphaDenomintor = 6;
}

View File

@@ -20,6 +20,10 @@ pragma solidity ^0.5.9;
interface IStaking {
/// @dev Initialize storage owned by this contract.
/// This function should not be called directly.
/// The StakingProxy contract will call it in `attachStakingContract()`.
function init() external;
/// @dev Pays a protocol fee in ETH.
/// @param makerAddress The address of the order's maker.

View File

@@ -53,12 +53,18 @@ interface IStakingEvents {
uint256 earliestEndTimeInSeconds
);
/// @dev Emitted by MixinExchangeFees when the cobb douglas alpha is updated.
/// @param numerator The alpha numerator.
/// @param denominator The alpha denominator.
event CobbDouglasAlphaChanged(
uint256 numerator,
uint256 denominator
/// @dev Emitted whenever hyperparameters are changed via the `tune()` function.
/// @param epochDurationInSeconds Minimum seconds between epochs.
/// @param rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm.
/// @param minimumPoolStake Minimum amount of stake required in a pool to collect rewards.
/// @param cobbDouglasAlphaNumerator Numerator for cobb douglas alpha factor.
/// @param cobbDouglasAlphaDenomintor Denominator for cobb douglas alpha factor.
event Tuned(
uint256 epochDurationInSeconds,
uint32 rewardDelegatedStakeWeight,
uint256 minimumPoolStake,
uint256 cobbDouglasAlphaNumerator,
uint256 cobbDouglasAlphaDenomintor
);
/// @dev Emitted by MixinScheduler when the timeLock period is changed.

View File

@@ -28,6 +28,21 @@ library LibStakingRichErrors {
MismatchedFeeAndPayment
}
enum InitializationErrorCode {
MixinSchedulerAlreadyInitialized
}
enum InvalidTuningValueErrorCode {
InvalidCobbDouglasAlpha
}
enum MakerPoolAssignmentErrorCodes {
MakerAddressAlreadyRegistered,
MakerAddressNotRegistered,
MakerAddressNotPendingAdd,
PoolIsFull
}
// bytes4(keccak256("MiscalculatedRewardsError(uint256,uint256)"))
bytes4 internal constant MISCALCULATED_REWARDS_ERROR_SELECTOR =
0xf7806c4e;
@@ -92,11 +107,7 @@ library LibStakingRichErrors {
bytes4 internal constant POOL_ALREADY_EXISTS_ERROR_SELECTOR =
0x2a5e4dcf;
// bytes4(keccak256("InvalidCobbDouglasAlphaError(uint256,uint256)"))
bytes4 internal constant INVALID_COBB_DOUGLAS_ALPHA_ERROR_SELECTOR =
0x8f8e73de;
// bytes4(keccak256("EthVaultNotSetError()"))
// bytes4(keccak256("EthVaultNotSetError()"))
bytes4 internal constant ETH_VAULT_NOT_SET_ERROR_SELECTOR =
0xa067f596;
@@ -112,6 +123,14 @@ library LibStakingRichErrors {
bytes internal constant PROXY_DESTINATION_CANNOT_BE_NIL =
hex"01ecebea";
// bytes4(keccak256("InitializationError(uint8)"))
bytes4 internal constant INITIALIZATION_ERROR_SELECTOR =
0x0b02d773;
// bytes4(keccak256("InvalidTuningValue(uint8)"))
bytes4 internal constant INVALID_TUNING_VALUE_ERROR_SELECTOR =
0xbbfd10bb;
// bytes4(keccak256("InvalidProtocolFeePaymentError(uint8,uint256,uint256)"))
bytes4 internal constant INVALID_PROTOCOL_FEE_PAYMENT_ERROR_SELECTOR =
0xefd6cb33;
@@ -355,21 +374,6 @@ library LibStakingRichErrors {
);
}
function InvalidCobbDouglasAlphaError(
uint256 numerator,
uint256 denominator
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
INVALID_COBB_DOUGLAS_ALPHA_ERROR_SELECTOR,
numerator,
denominator
);
}
function EthVaultNotSetError()
internal
pure
@@ -380,6 +384,16 @@ library LibStakingRichErrors {
);
}
function RewardVaultNotSetError()
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
REWARD_VAULT_NOT_SET_ERROR_SELECTOR
);
}
function InvalidProtocolFeePaymentError(
ProtocolFeePaymentErrorCodes errorCode,
uint256 expectedProtocolFeePaid,
@@ -397,16 +411,6 @@ library LibStakingRichErrors {
);
}
function RewardVaultNotSetError()
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
REWARD_VAULT_NOT_SET_ERROR_SELECTOR
);
}
function InvalidStakeStatusError(uint256 status)
internal
pure
@@ -418,6 +422,28 @@ library LibStakingRichErrors {
);
}
function InitializationError(InitializationErrorCode code)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
INITIALIZATION_ERROR_SELECTOR,
uint8(code)
);
}
function InvalidTuningValue(InvalidTuningValueErrorCode code)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
INVALID_TUNING_VALUE_ERROR_SELECTOR,
uint8(code)
);
}
function ProxyDestinationCannotBeNil()
internal
pure

View File

@@ -90,7 +90,7 @@ contract MixinStakingPool is
// Is the maker already in a pool?
if (isMakerAssignedToStakingPool(operatorAddress)) {
LibRichErrors.rrevert(LibStakingRichErrors.MakerPoolAssignmentError(
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MAKER_ADDRESS_ALREADY_REGISTERED,
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MakerAddressAlreadyRegistered,
operatorAddress,
getStakingPoolIdOfMaker(operatorAddress)
));
@@ -122,7 +122,7 @@ contract MixinStakingPool is
address makerAddress = msg.sender;
if (isMakerAssignedToStakingPool(makerAddress)) {
LibRichErrors.rrevert(LibStakingRichErrors.MakerPoolAssignmentError(
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MAKER_ADDRESS_ALREADY_REGISTERED,
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MakerAddressAlreadyRegistered,
makerAddress,
getStakingPoolIdOfMaker(makerAddress)
));
@@ -155,7 +155,7 @@ contract MixinStakingPool is
// Is the maker already in a pool?
if (isMakerAssignedToStakingPool(makerAddress)) {
LibRichErrors.rrevert(LibStakingRichErrors.MakerPoolAssignmentError(
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MAKER_ADDRESS_ALREADY_REGISTERED,
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MakerAddressAlreadyRegistered,
makerAddress,
getStakingPoolIdOfMaker(makerAddress)
));
@@ -165,16 +165,16 @@ contract MixinStakingPool is
bytes32 makerPendingPoolId = poolJoinedByMakerAddress[makerAddress].poolId;
if (makerPendingPoolId != poolId) {
LibRichErrors.rrevert(LibStakingRichErrors.MakerPoolAssignmentError(
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MAKER_ADDRESS_NOT_PENDING_ADD,
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MakerAddressNotPendingAdd,
makerAddress,
makerPendingPoolId
));
}
// Is the pool already full?
if (getNumberOfMakersInStakingPool(poolId) == MAX_MAKERS_IN_POOL) {
if (getNumberOfMakersInStakingPool(poolId) == maximumMakersInPool) {
LibRichErrors.rrevert(LibStakingRichErrors.MakerPoolAssignmentError(
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.POOL_IS_FULL,
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.PoolIsFull,
makerAddress,
poolId
));
@@ -210,7 +210,7 @@ contract MixinStakingPool is
bytes32 makerPoolId = getStakingPoolIdOfMaker(makerAddress);
if (makerPoolId != poolId) {
LibRichErrors.rrevert(LibStakingRichErrors.MakerPoolAssignmentError(
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MAKER_ADDRESS_NOT_REGISTERED,
LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MakerAddressNotRegistered,
makerAddress,
makerPoolId
));

View File

@@ -21,6 +21,7 @@ pragma solidity ^0.5.9;
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../libs/LibStakingRichErrors.sol";
import "../immutable/MixinHyperParameters.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinStorage.sol";
import "../interfaces/IStructs.sol";
@@ -36,7 +37,8 @@ import "../interfaces/IStakingEvents.sol";
contract MixinScheduler is
IStakingEvents,
MixinConstants,
MixinStorage
MixinStorage,
MixinHyperParameters
{
using LibSafeMath for uint256;
@@ -50,17 +52,6 @@ contract MixinScheduler is
return currentEpoch;
}
/// @dev Returns the current epoch period, measured in seconds.
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
/// @return Time in seconds.
function getEpochDurationInSeconds()
public
pure
returns (uint256)
{
return EPOCH_DURATION_IN_SECONDS;
}
/// @dev Returns the start time in seconds of the current epoch.
/// Epoch period = [startTimeInSeconds..endTimeInSeconds)
/// @return Time in seconds.
@@ -81,7 +72,22 @@ contract MixinScheduler is
view
returns (uint256)
{
return getCurrentEpochStartTimeInSeconds().safeAdd(getEpochDurationInSeconds());
return getCurrentEpochStartTimeInSeconds().safeAdd(epochDurationInSeconds);
}
/// @dev Initializes state owned by this mixin.
/// Fails if state was already initialized.
function _initMixinScheduler()
internal
{
if (currentEpochStartTimeInSeconds != 0) {
LibRichErrors._rrevert(
LibStakingRichErrors.InitializationError(
LibStakingRichErrors.InitializationErrorCode.MixinSchedulerAlreadyInitialized
)
);
}
currentEpochStartTimeInSeconds = block.timestamp;
}
/// @dev Moves to the next epoch, given the current epoch period has ended.
@@ -107,7 +113,9 @@ contract MixinScheduler is
uint256 nextEpoch = currentEpoch.safeAdd(1);
currentEpoch = nextEpoch;
currentEpochStartTimeInSeconds = currentBlockTimestamp;
uint256 earliestEndTimeInSeconds = currentEpochStartTimeInSeconds.safeAdd(getEpochDurationInSeconds());
uint256 earliestEndTimeInSeconds = currentEpochStartTimeInSeconds.safeAdd(
epochDurationInSeconds
);
// notify of epoch change
emit EpochChanged(

View File

@@ -20,10 +20,10 @@ import * as LibProxy from '../generated-artifacts/LibProxy.json';
import * as LibSafeDowncast from '../generated-artifacts/LibSafeDowncast.json';
import * as LibStakingRichErrors from '../generated-artifacts/LibStakingRichErrors.json';
import * as MixinConstants from '../generated-artifacts/MixinConstants.json';
import * as MixinDeploymentConstants from '../generated-artifacts/MixinDeploymentConstants.json';
import * as MixinEthVault from '../generated-artifacts/MixinEthVault.json';
import * as MixinExchangeFees from '../generated-artifacts/MixinExchangeFees.json';
import * as MixinExchangeManager from '../generated-artifacts/MixinExchangeManager.json';
import * as MixinHyperParameters from '../generated-artifacts/MixinHyperParameters.json';
import * as MixinScheduler from '../generated-artifacts/MixinScheduler.json';
import * as MixinStake from '../generated-artifacts/MixinStake.json';
import * as MixinStakeBalances from '../generated-artifacts/MixinStakeBalances.json';
@@ -52,7 +52,7 @@ export const artifacts = {
MixinExchangeFees: MixinExchangeFees as ContractArtifact,
MixinExchangeManager: MixinExchangeManager as ContractArtifact,
MixinConstants: MixinConstants as ContractArtifact,
MixinDeploymentConstants: MixinDeploymentConstants as ContractArtifact,
MixinHyperParameters: MixinHyperParameters as ContractArtifact,
MixinStorage: MixinStorage as ContractArtifact,
IEthVault: IEthVault as ContractArtifact,
IStaking: IStaking as ContractArtifact,

View File

@@ -18,10 +18,10 @@ export * from '../generated-wrappers/lib_proxy';
export * from '../generated-wrappers/lib_safe_downcast';
export * from '../generated-wrappers/lib_staking_rich_errors';
export * from '../generated-wrappers/mixin_constants';
export * from '../generated-wrappers/mixin_deployment_constants';
export * from '../generated-wrappers/mixin_eth_vault';
export * from '../generated-wrappers/mixin_exchange_fees';
export * from '../generated-wrappers/mixin_exchange_manager';
export * from '../generated-wrappers/mixin_hyper_parameters';
export * from '../generated-wrappers/mixin_scheduler';
export * from '../generated-wrappers/mixin_stake';
export * from '../generated-wrappers/mixin_stake_balances';

View File

@@ -18,10 +18,10 @@
"generated-artifacts/LibSafeDowncast.json",
"generated-artifacts/LibStakingRichErrors.json",
"generated-artifacts/MixinConstants.json",
"generated-artifacts/MixinDeploymentConstants.json",
"generated-artifacts/MixinEthVault.json",
"generated-artifacts/MixinExchangeFees.json",
"generated-artifacts/MixinExchangeManager.json",
"generated-artifacts/MixinHyperParameters.json",
"generated-artifacts/MixinScheduler.json",
"generated-artifacts/MixinStake.json",
"generated-artifacts/MixinStakeBalances.json",