diff --git a/package.json b/package.json index e96451454..a08e64ce4 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "hardhat-dependency-compiler": "1.1.3", "hardhat-deploy": "0.11.31", "hardhat-gas-reporter": "1.0.9", + "hardhat-tracer": "2.5.1", "husky": ">=6", "lerna": "^4.0.0", "lint-staged": ">=10", diff --git a/packages/common/hardhat.default.config.ts b/packages/common/hardhat.default.config.ts index 6c99a5fbc..3f56d2006 100644 --- a/packages/common/hardhat.default.config.ts +++ b/packages/common/hardhat.default.config.ts @@ -8,7 +8,9 @@ import '@nomicfoundation/hardhat-toolbox' import 'hardhat-contract-sizer' import 'hardhat-deploy' import 'hardhat-dependency-compiler' +import 'hardhat-tracer' import 'solidity-coverage' + import { getChainId, isArbitrum, isBase, isOptimism, SupportedChain } from './testutil/network' import { utils } from 'ethers' diff --git a/packages/perennial/contracts/types/Position.sol b/packages/perennial/contracts/types/Position.sol index ed8c70dc8..a37bb5227 100644 --- a/packages/perennial/contracts/types/Position.sol +++ b/packages/perennial/contracts/types/Position.sol @@ -209,6 +209,16 @@ library PositionLib { return _skew(self, riskParameter.virtualTaker); } + /// @notice Returns the skew of the position taking into account position socialization + /// @dev Used to calculate the portion of the position that is covered by the maker + /// @param self The position object to check + /// @return The socialized skew of the position + function socializedSkew(Position memory self) internal pure returns (UFixed6) { + return takerSocialized(self).isZero() ? + UFixed6Lib.ZERO : + takerSocialized(self).sub(minor(self)).div(takerSocialized(self)); + } + /// @notice Helper function to return the skew of the position with an optional virtual taker /// @param self The position object to check /// @param virtualTaker The virtual taker to use in the calculation diff --git a/packages/perennial/contracts/types/Version.sol b/packages/perennial/contracts/types/Version.sol index bc94f5d77..aa217ddc3 100644 --- a/packages/perennial/contracts/types/Version.sol +++ b/packages/perennial/contracts/types/Version.sol @@ -115,7 +115,7 @@ library VersionLib { ); // accumulate interest - (values.interestMaker,values.interestLong, values.interestShort, values.interestFee) = + (values.interestMaker, values.interestLong, values.interestShort, values.interestFee) = _accumulateInterest(self, fromPosition, fromOracleVersion, toOracleVersion, marketParameter, riskParameter); // accumulate P&L @@ -207,11 +207,11 @@ library VersionLib { // Redirect net portion of minor's side to maker if (fromPosition.long.gt(fromPosition.short)) { - fundingValues.fundingMaker = fundingValues.fundingShort.mul(Fixed6Lib.from(fromPosition.skew().abs())); + fundingValues.fundingMaker = fundingValues.fundingShort.mul(Fixed6Lib.from(fromPosition.socializedSkew())); fundingValues.fundingShort = fundingValues.fundingShort.sub(fundingValues.fundingMaker); } if (fromPosition.short.gt(fromPosition.long)) { - fundingValues.fundingMaker = fundingValues.fundingLong.mul(Fixed6Lib.from(fromPosition.skew().abs())); + fundingValues.fundingMaker = fundingValues.fundingLong.mul(Fixed6Lib.from(fromPosition.socializedSkew())); fundingValues.fundingLong = fundingValues.fundingLong.sub(fundingValues.fundingMaker); } diff --git a/packages/perennial/test/unit/market/Market.test.ts b/packages/perennial/test/unit/market/Market.test.ts index 3c1922ecf..662733a71 100644 --- a/packages/perennial/test/unit/market/Market.test.ts +++ b/packages/perennial/test/unit/market/Market.test.ts @@ -193,6 +193,118 @@ const EXPECTED_INTEREST_25_123 = BigNumber.from(1755) const EXPECTED_INTEREST_FEE_25_123 = EXPECTED_INTEREST_25_123.div(10) const EXPECTED_INTEREST_WITHOUT_FEE_25_123 = EXPECTED_INTEREST_25_123.sub(EXPECTED_INTEREST_FEE_25_123) +// rate_0 = 0 +// rate_1 = rate_0 + (elapsed * skew / k) +// funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years +// (0 + (0 + 3600 * 0.50 / 40000)) / 2 * 3600 * 10 * 123 / (86400 * 365) = 3160 +const EXPECTED_FUNDING_1_10_123_ALL = BigNumber.from(3160) +const EXPECTED_FUNDING_FEE_1_10_123_ALL = BigNumber.from(320) // (3159 + 157) = 3316 / 5 -> 664 * 5 -> 3320 +const EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL = EXPECTED_FUNDING_1_10_123_ALL.add( + EXPECTED_FUNDING_FEE_1_10_123_ALL.div(2), +) +const EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL = EXPECTED_FUNDING_1_10_123_ALL.sub( + EXPECTED_FUNDING_FEE_1_10_123_ALL.div(2), +) + +// rate_0 = 0.09 +// rate_1 = rate_0 + (elapsed * skew / k) +// funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years +// (0.045 + (0.045 + 3600 * 0.75 / 40000)) / 2 * 3600 * 10 * 123 / (86400 * 365) = 11060 +const EXPECTED_FUNDING_2_10_123_ALL = BigNumber.from(11060) +const EXPECTED_FUNDING_FEE_2_10_123_ALL = BigNumber.from(1100) // (11057 + 552) = 11609 / 10 -> 1161 * 10 -> 11610 - 11060 -> 550 * 2 -> 1100 +const EXPECTED_FUNDING_WITH_FEE_2_10_123_ALL = EXPECTED_FUNDING_2_10_123_ALL.add( + EXPECTED_FUNDING_FEE_2_10_123_ALL.div(2), +) +const EXPECTED_FUNDING_WITHOUT_FEE_2_10_123_ALL = EXPECTED_FUNDING_2_10_123_ALL.sub( + EXPECTED_FUNDING_FEE_2_10_123_ALL.div(2), +) + +// rate_0 = 0.09 +// rate_1 = rate_0 + (elapsed * skew / k) +// funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years +// (0.045 + (0.045 + 3600 * 0.50 / 40000)) / 2 * 3600 * 10 * 45 / (86400 * 365) = 3470 +const EXPECTED_FUNDING_2_10_45_ALL = BigNumber.from(3470) +const EXPECTED_FUNDING_FEE_2_10_45_ALL = BigNumber.from(350) +const EXPECTED_FUNDING_WITH_FEE_2_10_45_ALL = EXPECTED_FUNDING_2_10_45_ALL.add(EXPECTED_FUNDING_FEE_2_10_45_ALL.div(2)) +const EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL = EXPECTED_FUNDING_2_10_45_ALL.sub( + EXPECTED_FUNDING_FEE_2_10_45_ALL.div(2), +) + +// rate_0 = 0.09 +// rate_1 = rate_0 + (elapsed * skew / k) +// funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years +// (0.045 + (0.045 + 3600 * 0.50 / 40000)) / 2 * 3600 * 10 * 33 / (86400 * 365) = 2550 +const EXPECTED_FUNDING_2_10_33_ALL = BigNumber.from(2550) +const EXPECTED_FUNDING_FEE_2_10_33_ALL = BigNumber.from(255) +const EXPECTED_FUNDING_WITH_FEE_2_10_33_ALL = EXPECTED_FUNDING_2_10_33_ALL.add(EXPECTED_FUNDING_FEE_2_10_33_ALL.div(2)) +const EXPECTED_FUNDING_WITHOUT_FEE_2_10_33_ALL = EXPECTED_FUNDING_2_10_33_ALL.sub( + EXPECTED_FUNDING_FEE_2_10_33_ALL.div(2), +) + +// rate_0 = 0.09 +// rate_1 = rate_0 + (elapsed * skew / k) +// funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years +// (0.045 + (0.045 + 3600 * 0.50 / 40000)) / 2 * 3600 * 10 * 96 / (86400 * 365) = 7400 +const EXPECTED_FUNDING_2_10_96_ALL = BigNumber.from(7400) +const EXPECTED_FUNDING_FEE_2_10_96_ALL = BigNumber.from(740) +const EXPECTED_FUNDING_WITH_FEE_2_10_96_ALL = EXPECTED_FUNDING_2_10_96_ALL.add(EXPECTED_FUNDING_FEE_2_10_96_ALL.div(2)) +const EXPECTED_FUNDING_WITHOUT_FEE_2_10_96_ALL = EXPECTED_FUNDING_2_10_96_ALL.sub( + EXPECTED_FUNDING_FEE_2_10_96_ALL.div(2), +) + +// rate_0 = 0.09 +// rate_1 = rate_0 + (elapsed * skew / k) +// funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years +// (0.09 + (0.09 + 3600 * 0.50 / 40000)) / 2 * 3600 * 5 * 123 / (86400 * 365) = 7900 +const EXPECTED_FUNDING_3_10_123_ALL = BigNumber.from(7900) +const EXPECTED_FUNDING_FEE_3_10_123_ALL = BigNumber.from(790) +const EXPECTED_FUNDING_WITH_FEE_3_10_123_ALL = EXPECTED_FUNDING_3_10_123_ALL.add( + EXPECTED_FUNDING_FEE_3_10_123_ALL.div(2), +) +const EXPECTED_FUNDING_WITHOUT_FEE_3_10_123_ALL = EXPECTED_FUNDING_3_10_123_ALL.sub( + EXPECTED_FUNDING_FEE_3_10_123_ALL.div(2), +) + +// rate * elapsed * utilization * min(maker, taker) * price +// (0.4 / 365 / 24 / 60 / 60 ) * 3600 * 10 * 123 = 56170 +const EXPECTED_INTEREST_10_67_123_ALL = BigNumber.from(56170) +const EXPECTED_INTEREST_FEE_10_67_123_ALL = EXPECTED_INTEREST_10_67_123_ALL.div(10) +const EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL = EXPECTED_INTEREST_10_67_123_ALL.sub( + EXPECTED_INTEREST_FEE_10_67_123_ALL, +) + +// rate * elapsed * utilization * min(maker, taker) * price +// (0.64 / 365 / 24 / 60 / 60 ) * 3600 * 10 * 123 = 89870 +const EXPECTED_INTEREST_10_80_123_ALL = BigNumber.from(89870) +const EXPECTED_INTEREST_FEE_10_80_123_ALL = EXPECTED_INTEREST_10_80_123_ALL.div(10) +const EXPECTED_INTEREST_WITHOUT_FEE_10_80_123_ALL = EXPECTED_INTEREST_10_80_123_ALL.sub( + EXPECTED_INTEREST_FEE_10_80_123_ALL, +) + +// rate * elapsed * utilization * min(maker, taker) * price +// (0.4 / 365 / 24 / 60 / 60 ) * 3600 * 10 * 45 = 20550 +const EXPECTED_INTEREST_10_67_45_ALL = BigNumber.from(20550) +const EXPECTED_INTEREST_FEE_10_67_45_ALL = EXPECTED_INTEREST_10_67_45_ALL.div(10) +const EXPECTED_INTEREST_WITHOUT_FEE_10_67_45_ALL = EXPECTED_INTEREST_10_67_45_ALL.sub( + EXPECTED_INTEREST_FEE_10_67_45_ALL, +) + +// rate * elapsed * utilization * min(maker, taker) * price +// (0.4 / 365 / 24 / 60 / 60 ) * 3600 * 10 * 33 = 15070 +const EXPECTED_INTEREST_10_67_33_ALL = BigNumber.from(15070) +const EXPECTED_INTEREST_FEE_10_67_33_ALL = EXPECTED_INTEREST_10_67_33_ALL.div(10) +const EXPECTED_INTEREST_WITHOUT_FEE_10_67_33_ALL = EXPECTED_INTEREST_10_67_33_ALL.sub( + EXPECTED_INTEREST_FEE_10_67_33_ALL, +) + +// rate * elapsed * utilization * min(maker, taker) * price +// (0.4 / 365 / 24 / 60 / 60 ) * 3600 * 10 * 96 = 43840 +const EXPECTED_INTEREST_10_67_96_ALL = BigNumber.from(43840) +const EXPECTED_INTEREST_FEE_10_67_96_ALL = EXPECTED_INTEREST_10_67_96_ALL.div(10) +const EXPECTED_INTEREST_WITHOUT_FEE_10_67_96_ALL = EXPECTED_INTEREST_10_67_96_ALL.sub( + EXPECTED_INTEREST_FEE_10_67_96_ALL, +) + async function settle(market: Market, account: SignerWithAddress) { const local = await market.locals(account.address) const currentPosition = await market.pendingPositions(account.address, local.currentId) @@ -7678,6 +7790,3027 @@ describe.only('Market', () => { }) }) + context('all positions', async () => { + beforeEach(async () => { + dsu.transferFrom.whenCalledWith(user.address, market.address, COLLATERAL.mul(1e12)).returns(true) + }) + + context('position delta', async () => { + context('open', async () => { + beforeEach(async () => { + dsu.transferFrom.whenCalledWith(userB.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userB).update(userB.address, POSITION, 0, 0, COLLATERAL, false) + dsu.transferFrom.whenCalledWith(userC.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userC).update(userC.address, 0, 0, POSITION, COLLATERAL, false) + }) + + it('opens the position', async () => { + await expect(market.connect(user).update(user.address, 0, POSITION, 0, COLLATERAL, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, POSITION, 0, COLLATERAL, false) + + expectLocalEq(await market.locals(user.address), { + currentId: 1, + latestId: 0, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_1.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 1), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 1, + latestId: 0, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_1.timestamp, + }) + expectPositionEq(await market.pendingPosition(1), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_1.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('opens the position and settles', async () => { + await expect(market.connect(user).update(user.address, 0, POSITION, 0, COLLATERAL, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, POSITION, 0, COLLATERAL, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + + expectLocalEq(await market.locals(user.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION, + }) + expectPositionEq(await market.pendingPositions(user.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 2, + latestId: 1, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_2.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('opens a second position (same version)', async () => { + await market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false) + + await expect(market.connect(user).update(user.address, 0, POSITION, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, POSITION, 0, 0, false) + + expectLocalEq(await market.locals(user.address), { + currentId: 1, + latestId: 0, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_1.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 1), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 1, + latestId: 0, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_1.timestamp, + }) + expectPositionEq(await market.pendingPosition(1), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_2.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('opens a second position and settles (same version)', async () => { + await market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false) + + await expect(market.connect(user).update(user.address, 0, POSITION, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, POSITION, 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + + expectLocalEq(await market.locals(user.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION, + }) + expectPositionEq(await market.pendingPositions(user.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 2, + latestId: 1, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_2.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('opens a second position (next version)', async () => { + await market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await expect(market.connect(user).update(user.address, 0, POSITION, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_3.timestamp, 0, POSITION, 0, 0, false) + + expectLocalEq(await market.locals(user.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 2, + latestId: 1, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_2.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('opens a second position and settles (next version)', async () => { + await market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await expect(market.connect(user).update(user.address, 0, POSITION, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_3.timestamp, 0, POSITION, 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + oracle.status.returns([ORACLE_VERSION_3, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) // 50% to long, 50% to maker + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) // 33% from long, 67% from short + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION, + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + long: POSITION, + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + long: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + + it('opens the position and settles later', async () => { + await expect(market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, POSITION.div(2), 0, COLLATERAL, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + oracle.status.returns([ORACLE_VERSION_3, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) // 50% to long, 50% to maker + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) // 33% from long, 67% from short + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 2, + latestId: 1, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + + it('opens the position and settles later with fee', async () => { + const riskParameter = { ...(await market.riskParameter()) } + riskParameter.takerFee = parse6decimal('0.01') + riskParameter.takerImpactFee = parse6decimal('0.004') + riskParameter.takerSkewFee = parse6decimal('0.002') + await market.updateRiskParameter(riskParameter) + + const marketParameter = { ...(await market.parameter()) } + marketParameter.settlementFee = parse6decimal('0.50') + await market.updateParameter(marketParameter) + + const TAKER_FEE = parse6decimal('5.535') // position * (0.01 - 0.002 - 0.001) * price + const SETTLEMENT_FEE = parse6decimal('0.50') + + await expect(market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, POSITION.div(2), 0, COLLATERAL, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + oracle.status.returns([ORACLE_VERSION_3, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) // 50% to long, 50% to maker + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) // 33% from long, 67% from short + .sub(TAKER_FEE) + .sub(SETTLEMENT_FEE) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL).add(TAKER_FEE) + expectGlobalEq(await market.global(), { + currentId: 2, + latestId: 1, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10).add(SETTLEMENT_FEE), // loss of precision + riskFee: totalFee.div(2).div(10), // loss of precision + donation: totalFee.div(2).mul(8).div(10), // loss of precision + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + }) + + context('close', async () => { + beforeEach(async () => { + dsu.transferFrom.whenCalledWith(userB.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userB).update(userB.address, POSITION, 0, 0, COLLATERAL, false) + + dsu.transferFrom.whenCalledWith(userC.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userC).update(userC.address, 0, 0, POSITION, COLLATERAL, false) + + await market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false) + }) + + it('closes the position partially', async () => { + await expect(market.connect(user).update(user.address, 0, POSITION.div(4), 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, POSITION.div(4), 0, 0, false) + + expectLocalEq(await market.locals(user.address), { + currentId: 1, + latestId: 0, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_1.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 1), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION.div(4), + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 1, + latestId: 0, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_1.timestamp, + }) + expectPositionEq(await market.pendingPosition(1), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION.div(4), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_1.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('closes the position', async () => { + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, 0, 0, 0, false) + + expectLocalEq(await market.locals(user.address), { + currentId: 1, + latestId: 0, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_1.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 1), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 1, + latestId: 0, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_1.timestamp, + }) + expectPositionEq(await market.pendingPosition(1), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_1.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + context('settles first', async () => { + beforeEach(async () => { + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + }) + + it('closes the position', async () => { + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_3.timestamp, 0, 0, 0, 0, false) + + expectLocalEq(await market.locals(user.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 2, + latestId: 1, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_2.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('closes the position and settles', async () => { + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_3.timestamp, 0, 0, 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + oracle.status.returns([ORACLE_VERSION_3, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + + it('closes a second position (same version)', async () => { + await market.connect(user).update(user.address, 0, POSITION.div(4), 0, 0, false) + + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_3.timestamp, 0, 0, 0, 0, false) + + expectLocalEq(await market.locals(user.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 2, + latestId: 1, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_2.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('closes a second position and settles (same version)', async () => { + await market.connect(user).update(user.address, 0, POSITION.div(4), 0, 0, false) + + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_3.timestamp, 0, 0, 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + oracle.status.returns([ORACLE_VERSION_3, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + + it('closes a second position (next version)', async () => { + await market.connect(user).update(user.address, 0, POSITION.div(4), 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + oracle.status.returns([ORACLE_VERSION_3, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_4.timestamp, 0, 0, 0, 0, false) + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION.div(4), + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION.div(4), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + + it('closes a second position and settles (next version)', async () => { + await market.connect(user).update(user.address, 0, POSITION.div(4), 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + oracle.status.returns([ORACLE_VERSION_3, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_4.timestamp, 0, 0, 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_4.timestamp).returns(ORACLE_VERSION_4) + oracle.status.returns([ORACLE_VERSION_4, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 4, + latestId: 3, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_123_ALL.div(4)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(EXPECTED_INTEREST_10_80_123_ALL.div(5)) + .sub(3), // loss of precision + reward: EXPECTED_REWARD.mul(2).mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 4), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_123_ALL.mul(3).div(4)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_80_123_ALL) + .sub(38), // loss of precision + reward: EXPECTED_REWARD.mul(3).mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_FUNDING_FEE_2_10_123_ALL) + .add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + .add(EXPECTED_INTEREST_FEE_10_80_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 4, + latestId: 3, + protocolFee: totalFee.div(2).sub(2), // loss of precision + oracleFee: totalFee.div(2).div(10).sub(1), // loss of precision + riskFee: totalFee.div(2).div(10).sub(1), // loss of precision + donation: totalFee.div(2).mul(8).div(10).add(3), // loss of precision + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(4), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_4.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_123_ALL.mul(3).div(4)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_80_123_ALL) + .div(10) + .sub(3), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .add( + EXPECTED_FUNDING_WITHOUT_FEE_2_10_123_ALL.div(4) + .sub(EXPECTED_INTEREST_10_80_123_ALL.div(5)) + .mul(2) + .div(5), + ) + .sub(2), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .add( + EXPECTED_FUNDING_WITH_FEE_2_10_123_ALL.add(EXPECTED_INTEREST_10_80_123_ALL.mul(4).div(5)).div( + 10, + ), + ) + .mul(-1) + .sub(2), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).mul(2).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5).add(EXPECTED_REWARD.mul(2).mul(2).div(5)) }, + shortReward: { _value: EXPECTED_REWARD.mul(2).div(10) }, + }) + }) + + it('closes the position and settles later', async () => { + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_3.timestamp, 0, 0, 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + + oracle.at.whenCalledWith(ORACLE_VERSION_4.timestamp).returns(ORACLE_VERSION_4) + oracle.status.returns([ORACLE_VERSION_4, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + delta: COLLATERAL, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + + it('closes the position and settles later with fee', async () => { + const riskParameter = { ...(await market.riskParameter()) } + riskParameter.takerFee = parse6decimal('0.01') + riskParameter.takerImpactFee = parse6decimal('0.004') + riskParameter.takerSkewFee = parse6decimal('0.002') + await market.updateRiskParameter(riskParameter) + + const marketParameter = { ...(await market.parameter()) } + marketParameter.settlementFee = parse6decimal('0.50') + await market.updateParameter(marketParameter) + + const TAKER_FEE = parse6decimal('7.995') // position * (0.01 + 0.002 + 0.001) * price + const TAKER_FEE_FEE = TAKER_FEE.div(10) + const TAKER_FEE_WITHOUT_FEE = TAKER_FEE.sub(TAKER_FEE_FEE) + const SETTLEMENT_FEE = parse6decimal('0.50') + + await expect(market.connect(user).update(user.address, 0, 0, 0, 0, false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_3.timestamp, 0, 0, 0, 0, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_3.timestamp).returns(ORACLE_VERSION_3) + + oracle.at.whenCalledWith(ORACLE_VERSION_4.timestamp).returns(ORACLE_VERSION_4) + oracle.status.returns([ORACLE_VERSION_4, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(TAKER_FEE) + .sub(SETTLEMENT_FEE) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + delta: COLLATERAL, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(TAKER_FEE_WITHOUT_FEE) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + }) + }) + }) + + context('price delta', async () => { + beforeEach(async () => { + dsu.transferFrom.whenCalledWith(userB.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userB).update(userB.address, POSITION, 0, 0, COLLATERAL, false) + dsu.transferFrom.whenCalledWith(userC.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userC).update(userC.address, 0, 0, POSITION, COLLATERAL, false) + await market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + }) + + it('same price same timestamp settle', async () => { + const oracleVersionSameTimestamp = { + price: PRICE, + timestamp: TIMESTAMP + 3600, + valid: true, + } + + oracle.at.whenCalledWith(oracleVersionSameTimestamp.timestamp).returns(oracleVersionSameTimestamp) + oracle.status.returns([oracleVersionSameTimestamp, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 2, + latestId: 1, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 2, + latestId: 1, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_2.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(2), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + + it('lower price same rate settle', async () => { + dsu.balanceOf.whenCalledWith(market.address).returns(COLLATERAL.mul(1e12).mul(2)) + + const EXPECTED_PNL = parse6decimal('2').mul(10) // maker pnl + + const oracleVersionLowerPrice = { + price: parse6decimal('121'), + timestamp: TIMESTAMP + 7200, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionLowerPrice.timestamp).returns(oracleVersionLowerPrice) + oracle.status.returns([oracleVersionLowerPrice, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await expect(settle(market, user)) + .to.emit(market, 'PositionProcessed') + .withArgs(ORACLE_VERSION_2.timestamp, oracleVersionLowerPrice.timestamp, 1, 2, { + ...DEFAULT_VERSION_ACCUMULATION_RESULT, + fundingMaker: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2), + fundingLong: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2).add(1), // loss of precision + fundingShort: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.mul(-1).add(4), // loss of precision + fundingFee: EXPECTED_FUNDING_FEE_1_10_123_ALL.sub(5), // loss of precision + interestMaker: EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL.sub(5), // loss of precision + interestLong: EXPECTED_INTEREST_10_67_123_ALL.div(3).mul(-1).add(2), // loss of precision + interestShort: EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3).mul(-1).add(3), + interestFee: EXPECTED_INTEREST_FEE_10_67_123_ALL.sub(1), // loss of precision + pnlMaker: EXPECTED_PNL.div(2).mul(-1), + pnlLong: EXPECTED_PNL.div(2).mul(-1), + pnlShort: EXPECTED_PNL, + rewardMaker: EXPECTED_REWARD.mul(3), + rewardLong: EXPECTED_REWARD.mul(2), + rewardShort: EXPECTED_REWARD, + }) + .to.emit(market, 'AccountPositionProcessed') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, oracleVersionLowerPrice.timestamp, 1, 2, { + ...DEFAULT_LOCAL_ACCUMULATION_RESULT, + collateralAmount: EXPECTED_PNL.div(2) + .mul(-1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(2), // loss of precision + rewardAmount: EXPECTED_REWARD.mul(2), + }) + + await expect(settle(market, userB)) + .to.emit(market, 'AccountPositionProcessed') + .withArgs(userB.address, ORACLE_VERSION_2.timestamp, oracleVersionLowerPrice.timestamp, 1, 2, { + ...DEFAULT_LOCAL_ACCUMULATION_RESULT, + collateralAmount: EXPECTED_PNL.div(2) + .mul(-1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + rewardAmount: EXPECTED_REWARD.mul(3), + }) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.sub(EXPECTED_PNL.div(2)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.sub(EXPECTED_PNL.div(2)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_PNL.div(2) + .mul(-1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(2), // loss of precision + }, + longValue: { + _value: EXPECTED_PNL.div(2) + .mul(-1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_PNL.sub(EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL) + .sub(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + + it('higher price same rate settle', async () => { + const EXPECTED_PNL = parse6decimal('-2').mul(10) + + const oracleVersionHigherPrice = { + price: parse6decimal('125'), + timestamp: TIMESTAMP + 7200, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionHigherPrice.timestamp).returns(oracleVersionHigherPrice) + oracle.status.returns([oracleVersionHigherPrice, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await expect(settle(market, user)) + .to.emit(market, 'PositionProcessed') + .withArgs(ORACLE_VERSION_2.timestamp, oracleVersionHigherPrice.timestamp, 1, 2, { + ...DEFAULT_VERSION_ACCUMULATION_RESULT, + fundingMaker: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2), + fundingLong: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2).add(1), // loss of precision + fundingShort: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.mul(-1).add(4), // loss of precision + fundingFee: EXPECTED_FUNDING_FEE_1_10_123_ALL.sub(5), // loss of precision + interestMaker: EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL.sub(5), // loss of precision + interestLong: EXPECTED_INTEREST_10_67_123_ALL.div(3).mul(-1).add(2), // loss of precision + interestShort: EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3).mul(-1).add(3), + interestFee: EXPECTED_INTEREST_FEE_10_67_123_ALL.sub(1), // loss of precision + pnlMaker: EXPECTED_PNL.div(2).mul(-1), + pnlLong: EXPECTED_PNL.div(2).mul(-1), + pnlShort: EXPECTED_PNL, + rewardMaker: EXPECTED_REWARD.mul(3), + rewardLong: EXPECTED_REWARD.mul(2), + rewardShort: EXPECTED_REWARD, + }) + .to.emit(market, 'AccountPositionProcessed') + .withArgs(user.address, ORACLE_VERSION_2.timestamp, oracleVersionHigherPrice.timestamp, 1, 2, { + ...DEFAULT_LOCAL_ACCUMULATION_RESULT, + collateralAmount: EXPECTED_PNL.div(2) + .mul(-1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(2), // loss of precision + rewardAmount: EXPECTED_REWARD.mul(2), + }) + + await expect(settle(market, userB)) + .to.emit(market, 'AccountPositionProcessed') + .withArgs(userB.address, ORACLE_VERSION_2.timestamp, oracleVersionHigherPrice.timestamp, 1, 2, { + ...DEFAULT_LOCAL_ACCUMULATION_RESULT, + collateralAmount: EXPECTED_PNL.div(2) + .mul(-1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + rewardAmount: EXPECTED_REWARD.mul(3), + }) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.sub(EXPECTED_PNL.div(2)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.sub(EXPECTED_PNL.div(2)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_PNL.div(2) + .mul(-1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .div(10) + .sub(1), // loss of precision + }, + longValue: { + _value: EXPECTED_PNL.div(2) + .mul(-1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .div(5), + }, + shortValue: { + _value: EXPECTED_PNL.sub(EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL) + .sub(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .div(10) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + }) + }) + + context('liquidation', async () => { + context('maker', async () => { + beforeEach(async () => { + dsu.transferFrom.whenCalledWith(userB.address, market.address, utils.parseEther('450')).returns(true) + await market.connect(userB).update(userB.address, POSITION, 0, 0, parse6decimal('450'), false) + dsu.transferFrom.whenCalledWith(userC.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userC).update(userC.address, 0, 0, POSITION, COLLATERAL, false) + + dsu.transferFrom.whenCalledWith(user.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(user).update(user.address, 0, POSITION.div(2), 0, COLLATERAL, false) + }) + + it('with socialization to zero', async () => { + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + const EXPECTED_PNL = parse6decimal('78').mul(5) + const EXPECTED_LIQUIDATION_FEE = parse6decimal('13.5') + + const oracleVersionHigherPrice = { + price: parse6decimal('45'), + timestamp: TIMESTAMP + 7200, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionHigherPrice.timestamp).returns(oracleVersionHigherPrice) + oracle.status.returns([oracleVersionHigherPrice, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, user) + + dsu.transfer.whenCalledWith(liquidator.address, EXPECTED_LIQUIDATION_FEE.mul(1e12)).returns(true) + dsu.balanceOf.whenCalledWith(market.address).returns(COLLATERAL.mul(1e12)) + await expect( + market.connect(liquidator).update(userB.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true), + ) + .to.emit(market, 'Updated') + .withArgs(userB.address, ORACLE_VERSION_4.timestamp, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true) + + oracle.at.whenCalledWith(ORACLE_VERSION_4.timestamp).returns(ORACLE_VERSION_4) + oracle.status.returns([ORACLE_VERSION_4, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + const oracleVersionHigherPrice2 = { + price: parse6decimal('45'), + timestamp: TIMESTAMP + 14400, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionHigherPrice2.timestamp).returns(oracleVersionHigherPrice2) + oracle.status.returns([oracleVersionHigherPrice2, oracleVersionHigherPrice2.timestamp + 3600]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 5, + latestId: 4, + collateral: COLLATERAL.sub(EXPECTED_PNL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_45_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_3_10_123_ALL) + .sub(9), // loss of precision + reward: EXPECTED_REWARD.mul(2).mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 5, + latestId: 4, + collateral: parse6decimal('450') + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_45_ALL) + .sub(EXPECTED_LIQUIDATION_FEE) + .sub(25), // loss of precision + reward: EXPECTED_REWARD.mul(3).mul(2), + protection: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + }) + expectPositionEq(await market.pendingPositions(userB.address, 5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + delta: parse6decimal('450').sub(EXPECTED_LIQUIDATION_FEE), + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_FEE_2_10_45_ALL) + .add(EXPECTED_INTEREST_FEE_10_67_45_ALL) + .add(EXPECTED_FUNDING_FEE_3_10_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 5, + latestId: 4, + protocolFee: totalFee.div(2).sub(7), // loss of precision + oracleFee: totalFee.div(2).div(10).sub(1), // loss of precision + riskFee: totalFee.div(2).div(10).sub(1), // loss of precision + donation: totalFee.div(2).mul(8).div(10).sub(1), // loss of precision + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(EXPECTED_PNL) + .div(10) + .sub(2), + }, // loss of precision + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(EXPECTED_PNL) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .sub(EXPECTED_PNL.mul(2)) + .div(10) + .mul(-1), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_4.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_45_ALL) + .div(10) + .sub(2), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_45_ALL.div(3)) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_2_10_45_ALL) + .add(EXPECTED_INTEREST_10_67_45_ALL.mul(2).div(3)) + .div(10) + .mul(-1), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10).mul(2) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5).mul(2) }, + shortReward: { _value: EXPECTED_REWARD.div(10).mul(2) }, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_5.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_45_ALL) + .div(10) + .sub(2), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_45_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_3_10_123_ALL) + .sub(EXPECTED_PNL) + .div(5) + .sub(2), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_2_10_45_ALL) + .add(EXPECTED_INTEREST_10_67_45_ALL.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_3_10_123_ALL) + .sub(EXPECTED_PNL) + .div(10) + .mul(-1), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10).mul(2) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5).mul(3) }, + shortReward: { _value: EXPECTED_REWARD.div(10).mul(3) }, + }) + }) + + it('with partial socialization', async () => { + // (0.258823 / 365 / 24 / 60 / 60 ) * 3600 * 12 * 123 = 43610 + const EXPECTED_INTEREST_1 = BigNumber.from(43610) + const EXPECTED_INTEREST_FEE_1 = EXPECTED_INTEREST_1.div(10) + const EXPECTED_INTEREST_WITHOUT_FEE_1 = EXPECTED_INTEREST_1.sub(EXPECTED_INTEREST_FEE_1) + + // (0.258823 / 365 / 24 / 60 / 60 ) * 3600 * 12 * 45 = 15960 + const EXPECTED_INTEREST_2 = BigNumber.from(15960) + const EXPECTED_INTEREST_FEE_2 = EXPECTED_INTEREST_2.div(10) + const EXPECTED_INTEREST_WITHOUT_FEE_2 = EXPECTED_INTEREST_2.sub(EXPECTED_INTEREST_FEE_2) + + // (1.00 / 365 / 24 / 60 / 60 ) * 3600 * 2 * 123 = 28090 + const EXPECTED_INTEREST_3 = BigNumber.from(28090) + const EXPECTED_INTEREST_FEE_3 = EXPECTED_INTEREST_3.div(10) + const EXPECTED_INTEREST_WITHOUT_FEE_3 = EXPECTED_INTEREST_3.sub(EXPECTED_INTEREST_FEE_3) + + // rate_0 = 0.09 + // rate_1 = rate_0 + (elapsed * skew / k) + // funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years + // (0.09 + (0.09 + 3600 * 0.50 / 40000)) / 2 * 3600 * 7 * 123 / (86400 * 365) = 11060 + const EXPECTED_FUNDING_3 = BigNumber.from(11060) + const EXPECTED_FUNDING_FEE_3 = BigNumber.from(1110) + const EXPECTED_FUNDING_WITH_FEE_3 = EXPECTED_FUNDING_3.add(EXPECTED_FUNDING_FEE_3.div(2)) + const EXPECTED_FUNDING_WITHOUT_FEE_3 = EXPECTED_FUNDING_3.sub(EXPECTED_FUNDING_FEE_3.div(2)) + + dsu.transferFrom.whenCalledWith(liquidator.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(liquidator).update(liquidator.address, POSITION.div(5), 0, 0, COLLATERAL, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + await settle(market, liquidator) + + const EXPECTED_PNL = parse6decimal('78').mul(5) + const EXPECTED_LIQUIDATION_FEE = parse6decimal('13.5') + + const oracleVersionHigherPrice = { + price: parse6decimal('45'), + timestamp: TIMESTAMP + 7200, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionHigherPrice.timestamp).returns(oracleVersionHigherPrice) + oracle.status.returns([oracleVersionHigherPrice, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, liquidator) + dsu.transfer.whenCalledWith(liquidator.address, EXPECTED_LIQUIDATION_FEE.mul(1e12)).returns(true) + dsu.balanceOf.whenCalledWith(market.address).returns(COLLATERAL.mul(1e12)) + await expect( + market.connect(liquidator).update(userB.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true), + ) + .to.emit(market, 'Updated') + .withArgs(userB.address, ORACLE_VERSION_4.timestamp, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true) + + oracle.at.whenCalledWith(ORACLE_VERSION_4.timestamp).returns(ORACLE_VERSION_4) + oracle.status.returns([ORACLE_VERSION_4, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + await settle(market, liquidator) + + const oracleVersionHigherPrice2 = { + price: parse6decimal('45'), + timestamp: TIMESTAMP + 14400, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionHigherPrice2.timestamp).returns(oracleVersionHigherPrice2) + oracle.status.returns([oracleVersionHigherPrice2, oracleVersionHigherPrice2.timestamp + 3600]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + await settle(market, liquidator) + + expectLocalEq(await market.locals(user.address), { + currentId: 5, + latestId: 4, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_1.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .sub(EXPECTED_INTEREST_2.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_3.mul(5).div(7)) + .sub(EXPECTED_INTEREST_3.div(3)) + .sub(EXPECTED_PNL) + .sub(6), // loss of precision + reward: EXPECTED_REWARD.mul(2).mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 5, + latestId: 4, + collateral: parse6decimal('450') + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.mul(5).div(12)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_1.mul(10).div(12)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.mul(5).div(12)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_2.mul(10).div(12)) + .sub(EXPECTED_LIQUIDATION_FEE) + .sub(19), // loss of precision + reward: EXPECTED_REWARD.mul(3).mul(2).mul(10).div(12), + protection: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + }) + expectPositionEq(await market.pendingPositions(userB.address, 5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + delta: parse6decimal('450').sub(EXPECTED_LIQUIDATION_FEE), + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_1) + .add(EXPECTED_FUNDING_FEE_2_10_45_ALL) + .add(EXPECTED_INTEREST_FEE_2) + .add(EXPECTED_FUNDING_FEE_3) + .add(EXPECTED_INTEREST_FEE_3) + expectGlobalEq(await market.global(), { + currentId: 5, + latestId: 4, + protocolFee: totalFee.div(2).sub(10), // loss of precision + oracleFee: totalFee.div(2).div(10).sub(2), // loss of precision + riskFee: totalFee.div(2).div(10).sub(2), // loss of precision + donation: totalFee.div(2).mul(8).div(10).sub(2), // loss of precision + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION.div(5), + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + maker: POSITION.div(5), + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_1) + .sub(EXPECTED_PNL) + .div(12) + .sub(1), + }, // loss of precision + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_1.div(3)) + .sub(EXPECTED_PNL) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_1.mul(2).div(3)) + .sub(EXPECTED_PNL.mul(2)) + .div(10) + .mul(-1), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(12) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_4.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_2) + .div(12) + .sub(2), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_1.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .sub(EXPECTED_INTEREST_2.div(3)) + .div(5) + .sub(2), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_1.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_2_10_45_ALL) + .add(EXPECTED_INTEREST_2.mul(2).div(3)) + .div(10) + .mul(-1) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(12).mul(2) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5).mul(2) }, + shortReward: { _value: EXPECTED_REWARD.div(10).mul(2) }, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_5.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_1) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_2) + .div(12) + .add( + EXPECTED_FUNDING_WITHOUT_FEE_3.mul(2) + .div(7) + .add(EXPECTED_INTEREST_WITHOUT_FEE_3) + .sub(EXPECTED_PNL.mul(2).div(5)) + .div(2), + ) + .sub(6), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_1.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_45_ALL.div(2)) + .sub(EXPECTED_INTEREST_2.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_3.mul(5).div(7)) + .sub(EXPECTED_INTEREST_3.div(3)) + .sub(EXPECTED_PNL) + .div(5) + .sub(2), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_1.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_2_10_45_ALL) + .add(EXPECTED_INTEREST_2.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_3) + .add(EXPECTED_INTEREST_3.mul(2).div(3)) + .sub(EXPECTED_PNL.mul(7).div(5)) + .div(10) + .mul(-1), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(12).mul(2).add(EXPECTED_REWARD.mul(3).div(2)) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5).mul(3) }, + shortReward: { _value: EXPECTED_REWARD.div(10).mul(3) }, + }) + }) + + it('with shortfall', async () => { + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + const EXPECTED_PNL = parse6decimal('90').mul(5) + const EXPECTED_LIQUIDATION_FEE = parse6decimal('9.9') + + const oracleVersionHigherPrice = { + price: parse6decimal('33'), + timestamp: TIMESTAMP + 7200, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionHigherPrice.timestamp).returns(oracleVersionHigherPrice) + oracle.status.returns([oracleVersionHigherPrice, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, user) + dsu.transfer.whenCalledWith(liquidator.address, EXPECTED_LIQUIDATION_FEE.mul(1e12)).returns(true) + dsu.balanceOf.whenCalledWith(market.address).returns(COLLATERAL.mul(1e12)) + + await expect( + market.connect(liquidator).update(userB.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true), + ) + .to.emit(market, 'Updated') + .withArgs(userB.address, ORACLE_VERSION_4.timestamp, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(EXPECTED_PNL) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 3, + latestId: 2, + collateral: parse6decimal('450') + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(EXPECTED_LIQUIDATION_FEE) + .sub(EXPECTED_PNL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + delta: parse6decimal('450').sub(EXPECTED_LIQUIDATION_FEE), + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(EXPECTED_PNL) + .div(10) + .sub(2), + }, // loss of precision + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(EXPECTED_PNL) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .sub(EXPECTED_PNL.mul(2)) + .div(10) + .mul(-1), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + + const oracleVersionHigherPrice2 = { + price: parse6decimal('33'), + timestamp: TIMESTAMP + 10800, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionHigherPrice2.timestamp).returns(oracleVersionHigherPrice2) + oracle.status.returns([oracleVersionHigherPrice2, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + const shortfall = parse6decimal('450') + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_33_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_33_ALL) + .sub(EXPECTED_LIQUIDATION_FEE) + .sub(EXPECTED_PNL) + .sub(27) // loss of precision + factory.operators.whenCalledWith(userB.address, liquidator.address).returns(false) + dsu.transferFrom + .whenCalledWith(liquidator.address, market.address, shortfall.mul(-1).mul(1e12)) + .returns(true) + await expect(market.connect(liquidator).update(userB.address, 0, 0, 0, shortfall.mul(-1), false)) + .to.emit(market, 'Updated') + .withArgs(userB.address, ORACLE_VERSION_5.timestamp, 0, 0, 0, shortfall.mul(-1), false) + + expectLocalEq(await market.locals(userB.address), { + currentId: 4, + latestId: 3, + collateral: 0, + reward: EXPECTED_REWARD.mul(3).mul(2), + protection: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.pendingPositions(userB.address, 4), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + delta: parse6decimal('450').sub(EXPECTED_LIQUIDATION_FEE).add(shortfall.mul(-1)), + }) + }) + }) + + context('long', async () => { + beforeEach(async () => { + dsu.transferFrom.whenCalledWith(userB.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userB).update(userB.address, POSITION, 0, 0, COLLATERAL, false) + dsu.transferFrom.whenCalledWith(userC.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userC).update(userC.address, 0, 0, POSITION, COLLATERAL, false) + dsu.transferFrom.whenCalledWith(user.address, market.address, utils.parseEther('195')).returns(true) + await market.connect(user).update(user.address, 0, POSITION.div(2), 0, parse6decimal('195'), false) + }) + + it('default', async () => { + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + const EXPECTED_PNL = parse6decimal('27').mul(5) + const EXPECTED_LIQUIDATION_FEE = parse6decimal('14.4') + + const oracleVersionLowerPrice = { + price: parse6decimal('96'), + timestamp: TIMESTAMP + 7200, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionLowerPrice.timestamp).returns(oracleVersionLowerPrice) + oracle.status.returns([oracleVersionLowerPrice, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, userB) + dsu.transfer.whenCalledWith(liquidator.address, EXPECTED_LIQUIDATION_FEE.mul(1e12)).returns(true) + dsu.balanceOf.whenCalledWith(market.address).returns(COLLATERAL.mul(1e12)) + + await expect( + market.connect(liquidator).update(user.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true), + ) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_4.timestamp, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true) + + oracle.at.whenCalledWith(ORACLE_VERSION_4.timestamp).returns(ORACLE_VERSION_4) + oracle.status.returns([ORACLE_VERSION_4, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + const oracleVersionLowerPrice2 = { + price: parse6decimal('96'), + timestamp: TIMESTAMP + 14400, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionLowerPrice2.timestamp).returns(oracleVersionLowerPrice2) + oracle.status.returns([oracleVersionLowerPrice2, oracleVersionLowerPrice2.timestamp + 3600]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + // rate_0 = 0.09 + // rate_1 = rate_0 + (elapsed * skew / k) + // funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years + // (0.09 + (0.09 + 3600 * 1.00 / 40000)) / 2 * 3600 * 10 * 123 / (86400 * 365) = 18960 + const EXPECTED_FUNDING_3 = BigNumber.from(18960) + const EXPECTED_FUNDING_FEE_3 = BigNumber.from(1896) + const EXPECTED_FUNDING_WITH_FEE_3 = EXPECTED_FUNDING_3.add(EXPECTED_FUNDING_FEE_3.div(2)) + const EXPECTED_FUNDING_WITHOUT_FEE_3 = EXPECTED_FUNDING_3.sub(EXPECTED_FUNDING_FEE_3.div(2)) + + // rate * elapsed * utilization * min(maker, taker) * price + // (1.00 / 365 / 24 / 60 / 60 ) * 3600 * 10 * 123 = 140410 + const EXPECTED_INTEREST_3 = BigNumber.from(140410) + const EXPECTED_INTEREST_FEE_3 = EXPECTED_INTEREST_3.div(10) + const EXPECTED_INTEREST_WITHOUT_FEE_3 = EXPECTED_INTEREST_3.sub(EXPECTED_INTEREST_FEE_3) + + expectLocalEq(await market.locals(user.address), { + currentId: 5, + latestId: 4, + collateral: parse6decimal('195') + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_96_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_96_ALL.div(3)) + .sub(EXPECTED_LIQUIDATION_FEE) + .sub(9), // loss of precision + reward: EXPECTED_REWARD.mul(2).mul(2), + protection: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + delta: parse6decimal('195').sub(EXPECTED_LIQUIDATION_FEE), + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 5, + latestId: 4, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_96_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_96_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_3) + .add(EXPECTED_INTEREST_WITHOUT_FEE_3) + .sub(EXPECTED_PNL.mul(2)) + .sub(45), // loss of precision + reward: EXPECTED_REWARD.mul(3).mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_FEE_2_10_96_ALL) + .add(EXPECTED_INTEREST_FEE_10_67_96_ALL) + .add(EXPECTED_FUNDING_FEE_3) + .add(EXPECTED_INTEREST_FEE_3) + expectGlobalEq(await market.global(), { + currentId: 5, + latestId: 4, + protocolFee: totalFee.div(2).sub(5), // loss of precision + oracleFee: totalFee.div(2).div(10).sub(1), // loss of precision + riskFee: totalFee.div(2).div(10).sub(1), // loss of precision + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION, + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(5), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_6.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(EXPECTED_PNL) + .div(10) + .sub(2), + }, // loss of precision + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(EXPECTED_PNL) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .sub(EXPECTED_PNL.mul(2)) + .mul(-1) + .div(10), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_4.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_96_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_96_ALL) + .div(10) + .sub(2), + }, // loss of precision + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_96_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_96_ALL.div(3)) + .div(5) + .sub(2), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_2_10_96_ALL) + .add(EXPECTED_INTEREST_10_67_96_ALL.mul(2).div(3)) + .mul(-1) + .div(10) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10).mul(2) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5).mul(2) }, + shortReward: { _value: EXPECTED_REWARD.div(10).mul(2) }, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_5.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_96_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_96_ALL) + .add(EXPECTED_FUNDING_WITHOUT_FEE_3) + .add(EXPECTED_INTEREST_WITHOUT_FEE_3) + .sub(EXPECTED_PNL.mul(2)) + .div(10) + .sub(5), + }, // loss of precision + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2_10_96_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_96_ALL.div(3)) + .div(5) + .sub(2), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_2_10_96_ALL) + .add(EXPECTED_INTEREST_10_67_96_ALL.mul(2).div(3)) + .add(EXPECTED_FUNDING_WITH_FEE_3) + .add(EXPECTED_INTEREST_3) + .sub(EXPECTED_PNL.mul(2)) + .mul(-1) + .div(10) + .sub(1), // loss of precision + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10).mul(3) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5).mul(2) }, + shortReward: { _value: EXPECTED_REWARD.div(10).mul(3) }, + }) + }) + + it('with shortfall', async () => { + const riskParameter = { ...(await market.riskParameter()) } + riskParameter.minMaintenance = parse6decimal('50') + await market.connect(owner).updateRiskParameter(riskParameter) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + const EXPECTED_PNL = parse6decimal('80').mul(5) + const EXPECTED_LIQUIDATION_FEE = parse6decimal('6.45') + + const oracleVersionLowerPrice = { + price: parse6decimal('43'), + timestamp: TIMESTAMP + 7200, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionLowerPrice.timestamp).returns(oracleVersionLowerPrice) + oracle.status.returns([oracleVersionLowerPrice, ORACLE_VERSION_4.timestamp]) + oracle.request.returns() + + await settle(market, userB) + dsu.transfer.whenCalledWith(liquidator.address, EXPECTED_LIQUIDATION_FEE.mul(1e12)).returns(true) + dsu.balanceOf.whenCalledWith(market.address).returns(COLLATERAL.mul(1e12)) + + await expect( + market.connect(liquidator).update(user.address, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true), + ) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_4.timestamp, 0, 0, 0, EXPECTED_LIQUIDATION_FEE.mul(-1), true) + + // rate_1 = rate_0 + (elapsed * skew / k) + // funding = (rate_0 + rate_1) / 2 * elapsed * taker * price / time_in_years + // (0.045 + (0.045 + 3600 * 0.5 / 40000)) / 2 * 3600 * 10 * 43 / (86400 * 365) = 3315 + const EXPECTED_FUNDING_2 = BigNumber.from(3315) + const EXPECTED_FUNDING_FEE_2 = BigNumber.from(330) + const EXPECTED_FUNDING_WITH_FEE_2 = EXPECTED_FUNDING_2.add(EXPECTED_FUNDING_FEE_2.div(2)) + const EXPECTED_FUNDING_WITHOUT_FEE_2 = EXPECTED_FUNDING_2.sub(EXPECTED_FUNDING_FEE_2.div(2)) + + // rate * elapsed * utilization * min(maker, taker) * price + // (0.40 / 365 / 24 / 60 / 60 ) * 3600 * 10 * 43 = 19640 + const EXPECTED_INTEREST_2 = BigNumber.from(19640) + const EXPECTED_INTEREST_FEE_2 = EXPECTED_INTEREST_2.div(10) + const EXPECTED_INTEREST_WITHOUT_FEE_2 = EXPECTED_INTEREST_2.sub(EXPECTED_INTEREST_FEE_2) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: parse6decimal('195') + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(EXPECTED_PNL) + .sub(EXPECTED_LIQUIDATION_FEE) + .sub(2), // loss of precision + reward: EXPECTED_REWARD.mul(2), + protection: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + delta: parse6decimal('195').sub(EXPECTED_LIQUIDATION_FEE), + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL.add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(EXPECTED_PNL) + .sub(13), // loss of precision + reward: EXPECTED_REWARD.mul(3), + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(userB.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + const totalFee = EXPECTED_FUNDING_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_FEE_10_67_123_ALL) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: totalFee.div(2).sub(3), // loss of precision + oracleFee: totalFee.div(2).div(10), + riskFee: totalFee.div(2).div(10), + donation: totalFee.div(2).mul(8).div(10), + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_3.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .add(EXPECTED_INTEREST_WITHOUT_FEE_10_67_123_ALL) + .sub(EXPECTED_PNL) + .div(10) + .sub(2), // loss of precision + }, + longValue: { + _value: EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .sub(EXPECTED_PNL) + .div(5) + .sub(1), // loss of precision + }, + shortValue: { + _value: EXPECTED_FUNDING_WITH_FEE_1_10_123_ALL.add(EXPECTED_INTEREST_10_67_123_ALL.mul(2).div(3)) + .sub(EXPECTED_PNL.mul(2)) + .div(10) + .mul(-1), + }, + makerReward: { _value: EXPECTED_REWARD.mul(3).div(10) }, + longReward: { _value: EXPECTED_REWARD.mul(2).div(5) }, + shortReward: { _value: EXPECTED_REWARD.div(10) }, + }) + + const oracleVersionLowerPrice2 = { + price: parse6decimal('43'), + timestamp: TIMESTAMP + 10800, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionLowerPrice2.timestamp).returns(oracleVersionLowerPrice2) + oracle.status.returns([oracleVersionLowerPrice2, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + const shortfall = parse6decimal('195') + .add(EXPECTED_FUNDING_WITHOUT_FEE_1_10_123_ALL.div(2)) + .sub(EXPECTED_INTEREST_10_67_123_ALL.div(3)) + .add(EXPECTED_FUNDING_WITHOUT_FEE_2.div(2)) + .sub(EXPECTED_INTEREST_2.div(3)) + .sub(EXPECTED_LIQUIDATION_FEE) + .sub(EXPECTED_PNL) + .sub(6) // loss of precision + dsu.transferFrom + .whenCalledWith(liquidator.address, market.address, shortfall.mul(-1).mul(1e12)) + .returns(true) + factory.operators.whenCalledWith(user.address, liquidator.address).returns(false) + await expect(market.connect(liquidator).update(user.address, 0, 0, 0, shortfall.mul(-1), false)) + .to.emit(market, 'Updated') + .withArgs(user.address, ORACLE_VERSION_5.timestamp, 0, 0, 0, shortfall.mul(-1), false) + + expectLocalEq(await market.locals(user.address), { + currentId: 4, + latestId: 3, + collateral: 0, + reward: EXPECTED_REWARD.mul(2).mul(2), + protection: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + }) + expectPositionEq(await market.pendingPositions(user.address, 4), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + delta: parse6decimal('195').sub(EXPECTED_LIQUIDATION_FEE).add(shortfall.mul(-1)), + }) + }) + }) + }) + + context('closed', async () => { + beforeEach(async () => { + await market.connect(user).update(user.address, POSITION, 0, 0, COLLATERAL, false) + dsu.transferFrom.whenCalledWith(userB.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userB).update(userB.address, 0, POSITION.div(2), 0, COLLATERAL, false) + dsu.transferFrom.whenCalledWith(userC.address, market.address, COLLATERAL.mul(1e12)).returns(true) + await market.connect(userC).update(userC.address, 0, 0, POSITION, COLLATERAL, false) + + oracle.at.whenCalledWith(ORACLE_VERSION_2.timestamp).returns(ORACLE_VERSION_2) + oracle.status.returns([ORACLE_VERSION_2, ORACLE_VERSION_3.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + }) + + it('zeroes PnL and fees (price change)', async () => { + const marketParameter = { ...(await market.parameter()) } + marketParameter.closed = true + await market.updateParameter(marketParameter) + + const oracleVersionHigherPrice_0 = { + price: parse6decimal('125'), + timestamp: TIMESTAMP + 7200, + valid: true, + } + const oracleVersionHigherPrice_1 = { + price: parse6decimal('128'), + timestamp: TIMESTAMP + 10800, + valid: true, + } + oracle.at.whenCalledWith(oracleVersionHigherPrice_0.timestamp).returns(oracleVersionHigherPrice_0) + + oracle.at.whenCalledWith(oracleVersionHigherPrice_1.timestamp).returns(oracleVersionHigherPrice_1) + oracle.status.returns([oracleVersionHigherPrice_1, ORACLE_VERSION_5.timestamp]) + oracle.request.returns() + + await settle(market, user) + await settle(market, userB) + + expectLocalEq(await market.locals(user.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(user.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + }) + expectPositionEq(await market.pendingPositions(user.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION, + delta: COLLATERAL, + }) + expectLocalEq(await market.locals(userB.address), { + currentId: 3, + latestId: 2, + collateral: COLLATERAL, + reward: 0, + protection: 0, + }) + expectPositionEq(await market.positions(userB.address), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + long: POSITION.div(2), + }) + expectPositionEq(await market.pendingPositions(userB.address, 3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + long: POSITION.div(2), + delta: COLLATERAL, + }) + expectGlobalEq(await market.global(), { + currentId: 3, + latestId: 2, + protocolFee: 0, + oracleFee: 0, + riskFee: 0, + donation: 0, + }) + expectPositionEq(await market.position(), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_4.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectPositionEq(await market.pendingPosition(3), { + ...DEFAULT_POSITION, + timestamp: ORACLE_VERSION_5.timestamp, + maker: POSITION, + long: POSITION.div(2), + short: POSITION, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_3.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + expectVersionEq(await market.versions(ORACLE_VERSION_4.timestamp), { + makerValue: { _value: 0 }, + longValue: { _value: 0 }, + shortValue: { _value: 0 }, + makerReward: { _value: 0 }, + longReward: { _value: 0 }, + shortReward: { _value: 0 }, + }) + }) + }) + }) + context('invariant violations', async () => { it('reverts if can liquidate', async () => { dsu.transferFrom.whenCalledWith(user.address, market.address, utils.parseEther('500')).returns(true) diff --git a/yarn.lock b/yarn.lock index 1eaa36512..e66c3800a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4405,7 +4405,7 @@ ethers@^4.0.40: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@^5.5.3, ethers@^5.7.1: +ethers@^5.5.3, ethers@^5.6.1, ethers@^5.7.1: version "5.7.2" resolved "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -5223,6 +5223,15 @@ hardhat-gas-reporter@1.0.9: eth-gas-reporter "^0.2.25" sha1 "^1.1.1" +hardhat-tracer@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/hardhat-tracer/-/hardhat-tracer-2.5.1.tgz#4f71b0d5e250e4acfe3a52304cf0ee30371a6022" + integrity sha512-0IDvoSyOCD+Lq+Vbq8fwHvhRDRodr74QpRAwU6V9RoJgiH4ohpqRpSlz2IhtPQcKSjDFsqOD55PyHCgZ578aUw== + dependencies: + chalk "^4.1.2" + debug "^4.3.4" + ethers "^5.6.1" + hardhat@2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.16.1.tgz#fd2288ce44f6846a70ba332b3d8158522447262a"