Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
[Fix][Sherlock M-3 + M-13] Handle position rebalance 0 value cases
Browse files Browse the repository at this point in the history
  • Loading branch information
arjun-io committed Jul 26, 2023
1 parent 9f153c9 commit 6ead0cc
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 18 deletions.
12 changes: 9 additions & 3 deletions packages/perennial-vaults/contracts/balanced/BalancedVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -511,16 +511,22 @@ contract BalancedVault is IBalancedVault, BalancedVaultDefinition, UInitializabl
UFixed18 makerAvailable = position.maker.gt(position.taker) ?
position.maker.sub(position.taker) : UFixed18Lib.ZERO;

product.closeMake(accountPosition.sub(targetPosition).min(makerAvailable));
// If there is no maker available (socialization), we still need a settlement but closing 0 value will revert,
// so instead open 0 value instead
if (makerAvailable.isZero()) product.openMake(makerAvailable);
else product.closeMake(accountPosition.sub(targetPosition).min(makerAvailable));
}

if (targetPosition.gt(accountPosition)) {
if (targetPosition.gte(accountPosition)) {
// compute headroom until hitting makerLimit
UFixed18 currentMaker = product.positionAtVersion(product.latestVersion()).next(product.pre()).maker;
UFixed18 makerLimit = product.makerLimit();
UFixed18 makerAvailable = makerLimit.gt(currentMaker) ? makerLimit.sub(currentMaker) : UFixed18Lib.ZERO;

product.openMake(targetPosition.sub(accountPosition).min(makerAvailable));
// If there is no maker available (maker limit), we still need a settlement but opening 0 value will revert,
// so instead close 0 value instead
if (makerAvailable.isZero()) product.closeMake(makerAvailable);
else product.openMake(targetPosition.sub(accountPosition).min(makerAvailable));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,14 +433,29 @@ contract SingleBalancedVault is ISingleBalancedVault, UInitializable {
*/
function _updateMakerPosition(IProduct product, UFixed18 targetPosition) private {
UFixed18 currentPosition = product.position(address(this)).next(product.pre(address(this))).maker;
UFixed18 currentMaker = product.positionAtVersion(product.latestVersion()).next(product.pre()).maker;
UFixed18 makerLimit = product.makerLimit();
UFixed18 makerAvailable = makerLimit.gt(currentMaker) ? makerLimit.sub(currentMaker) : UFixed18Lib.ZERO;

if (targetPosition.lt(currentPosition))
product.closeMake(currentPosition.sub(targetPosition));
if (targetPosition.gte(currentPosition))
product.openMake(targetPosition.sub(currentPosition).min(makerAvailable));

if (targetPosition.lt(currentPosition)) {
// compute headroom until hitting taker amount
Position memory position = product.positionAtVersion(product.latestVersion()).next(product.pre());
UFixed18 makerAvailable = position.maker.gt(position.taker) ?
position.maker.sub(position.taker) : UFixed18Lib.ZERO;

// If there is no maker available (socialization), we still need a settlement but closing 0 value will revert,
// so instead open 0 value instead
if (makerAvailable.isZero()) product.openMake(makerAvailable);
else product.closeMake(currentPosition.sub(targetPosition).min(makerAvailable));
}
if (targetPosition.gte(currentPosition)) {
// compute headroom until hitting makerLimit
UFixed18 currentMaker = product.positionAtVersion(product.latestVersion()).next(product.pre()).maker;
UFixed18 makerLimit = product.makerLimit();
UFixed18 makerAvailable = makerLimit.gt(currentMaker) ? makerLimit.sub(currentMaker) : UFixed18Lib.ZERO;

// If there is no maker available, we still need a settlement but opening 0-value will revert,
// so instead close a 0 value instead
if (makerAvailable.isZero()) product.closeMake(makerAvailable);
else product.openMake(targetPosition.sub(currentPosition).min(makerAvailable));
}

emit PositionUpdated(product, targetPosition);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import HRE from 'hardhat'
import { time, impersonate } from '../../../../common/testutil'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { anyUint } from '@nomicfoundation/hardhat-chai-matchers/withArgs'
import { FakeContract, smock } from '@defi-wonderland/smock'
import { expect, use } from 'chai'
import {
Expand All @@ -18,6 +19,7 @@ import {
ICollateral__factory,
} from '../../../types/generated'
import { BigNumber, constants, utils } from 'ethers'
import { impersonateWithBalance } from '../../../../common/testutil/impersonate'

const { config, ethers } = HRE
use(smock.matchers)
Expand All @@ -35,6 +37,7 @@ describe('BalancedVault', () => {
let user2: SignerWithAddress
let perennialUser: SignerWithAddress
let liquidator: SignerWithAddress
let marketOwner: SignerWithAddress
let long: IProduct
let short: IProduct
let leverage: BigNumber
Expand Down Expand Up @@ -120,7 +123,7 @@ describe('BalancedVault', () => {
await setUpWalletWithDSU(user)
await setUpWalletWithDSU(user2)
await setUpWalletWithDSU(liquidator)
await setUpWalletWithDSU(perennialUser, utils.parseEther('1000000'))
await setUpWalletWithDSU(perennialUser, utils.parseEther('10000000'))

// Unfortunately, we can't make mocks of existing contracts.
// So, we make a fake and initialize it with the values that the real contract had at this block.
Expand All @@ -134,6 +137,8 @@ describe('BalancedVault', () => {
oracle.sync.returns(currentVersion)
oracle.currentVersion.returns(currentVersion)
oracle.atVersion.whenCalledWith(currentVersion[0]).returns(currentVersion)

marketOwner = await impersonateWithBalance(await controller['owner(address)'](long.address), utils.parseEther('10'))
})

describe('#constructor', () => {
Expand Down Expand Up @@ -758,6 +763,42 @@ describe('BalancedVault', () => {
expect(await shortPosition()).to.equal(0)
})

it('closes 0 value position if above maker limit', async () => {
// Get maker product very close to the makerLimit
await asset.connect(perennialUser).approve(collateral.address, constants.MaxUint256)
await collateral
.connect(perennialUser)
.depositTo(perennialUser.address, short.address, utils.parseEther('400000'))
const shortMakerAvailable = (await short.makerLimit()).sub(
(await short.positionAtVersion(await short['latestVersion()']())).maker,
)
await collateral.connect(perennialUser).depositTo(perennialUser.address, long.address, utils.parseEther('400000'))
const longMakerAvailable = (await long.makerLimit()).sub(
(await long.positionAtVersion(await long['latestVersion()']())).maker,
)

await short.connect(perennialUser).openMake(shortMakerAvailable)
await long.connect(perennialUser).openMake(longMakerAvailable)
await updateOracle()
await vault.sync()

await short.connect(marketOwner).updateMakerLimit(0)
await long.connect(marketOwner).updateMakerLimit(0)

// Deposit should create a greater position than what's available
const largeDeposit = utils.parseEther('10000')
await vault.connect(user).deposit(largeDeposit, user.address)
await updateOracle()
await expect(vault.sync())
.to.emit(short, 'MakeClosed')
.withArgs(vault.address, anyUint, 0)
.to.emit(long, 'MakeClosed')
.withArgs(vault.address, anyUint, 0)

expect(await longPosition()).to.equal(0)
expect(await shortPosition()).to.equal(0)
})

it('close to taker', async () => {
// Deposit should create a greater position than what's available
const largeDeposit = utils.parseEther('10000')
Expand Down Expand Up @@ -786,6 +827,34 @@ describe('BalancedVault', () => {
)
})

it('opens 0 value position if in >=100% utilization', async () => {
// Deposit should create a greater position than what's available
const largeDeposit = utils.parseEther('10000')
await vault.connect(user).deposit(largeDeposit, user.address)
await updateOracle()
await vault.sync()

// Get taker product very close to the maker
await asset.connect(perennialUser).approve(collateral.address, constants.MaxUint256)
await collateral
.connect(perennialUser)
.depositTo(perennialUser.address, short.address, utils.parseEther('1000000'))

await short.settle()
const globalShort = await short.positionAtVersion(await short['latestVersion()']())
const shortAvailable = globalShort.maker.sub(globalShort.taker)
await short.connect(perennialUser).openTake(shortAvailable)
await updateOracle()
await short.settle()

// Increase the price to result in a profit for the vault on the short side, resulting in a close attempt on the next
// update
await updateOracle(originalOraclePrice.add(utils.parseEther('10')))
await vault.sync()
await vault.connect(user).redeem(await vault.maxRedeem(user.address), user.address)
await expect(vault.sync()).to.emit(short, 'MakeOpened').withArgs(vault.address, anyUint, 0)
})

it('product closing closes all positions', async () => {
const largeDeposit = utils.parseEther('10000')
await vault.connect(user).deposit(largeDeposit, user.address)
Expand Down Expand Up @@ -814,6 +883,18 @@ describe('BalancedVault', () => {
expect(await shortPosition()).to.be.greaterThan(0)
})

it('opens a 0 value position if account position is at target', async () => {
// Deposit should create a greater position than what's available
const largeDeposit = utils.parseEther('10000')
await vault.connect(user).deposit(largeDeposit, user.address)
await updateOracle()
await vault.sync()

await updateOracle()
await vault.connect(user).deposit(0, user.address)
await expect(vault.sync()).to.emit(short, 'MakeOpened').withArgs(vault.address, anyUint, 0)
})

context('liquidation', () => {
context('long', () => {
it('recovers before being liquidated', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import HRE from 'hardhat'
import { time, impersonate } from '../../../../common/testutil'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { anyUint } from '@nomicfoundation/hardhat-chai-matchers/withArgs'
import { FakeContract, smock } from '@defi-wonderland/smock'
import { expect, use } from 'chai'
import {
Expand All @@ -17,11 +18,12 @@ import {
ICollateral__factory,
} from '../../../types/generated'
import { BigNumber, constants, utils } from 'ethers'
import { impersonateWithBalance } from '../../../../common/testutil/impersonate'

const { config, ethers } = HRE
use(smock.matchers)

const DSU_HOLDER = '0xaef566ca7e84d1e736f999765a804687f39d9094'
const DSU_MINTER = '0xD05aCe63789cCb35B9cE71d01e4d632a0486Da4B'

describe('SingleBalancedVault', () => {
let vault: SingleBalancedVault
Expand All @@ -33,6 +35,7 @@ describe('SingleBalancedVault', () => {
let user2: SignerWithAddress
let perennialUser: SignerWithAddress
let liquidator: SignerWithAddress
let marketOwner: SignerWithAddress
let long: IProduct
let short: IProduct
let leverage: BigNumber
Expand Down Expand Up @@ -94,16 +97,21 @@ describe('SingleBalancedVault', () => {
await vault.initialize('Perennial Vault Alpha', 'PVA')
asset = IERC20Metadata__factory.connect(await vault.asset(), owner)

const dsuHolder = await impersonate.impersonateWithBalance(DSU_HOLDER, utils.parseEther('10'))
const setUpWalletWithDSU = async (wallet: SignerWithAddress) => {
await dsu.connect(dsuHolder).transfer(wallet.address, utils.parseEther('200000'))
const dsuMinter = await impersonate.impersonateWithBalance(DSU_MINTER, utils.parseEther('10'))
const setUpWalletWithDSU = async (wallet: SignerWithAddress, amount?: BigNumber) => {
const dsuIface = new utils.Interface(['function mint(uint256)'])
await dsuMinter.sendTransaction({
to: dsu.address,
value: 0,
data: dsuIface.encodeFunctionData('mint', [amount ?? utils.parseEther('200000')]),
})
await dsu.connect(dsuMinter).transfer(wallet.address, amount ?? utils.parseEther('200000'))
await dsu.connect(wallet).approve(vault.address, ethers.constants.MaxUint256)
}
await setUpWalletWithDSU(user)
await setUpWalletWithDSU(user2)
await setUpWalletWithDSU(liquidator)
await setUpWalletWithDSU(perennialUser)
await setUpWalletWithDSU(perennialUser)
await setUpWalletWithDSU(perennialUser, utils.parseEther('2000000'))

// Unfortunately, we can't make mocks of existing contracts.
// So, we make a fake and initialize it with the values that the real contract had at this block.
Expand All @@ -117,6 +125,8 @@ describe('SingleBalancedVault', () => {
oracle.sync.returns(currentVersion)
oracle.currentVersion.returns(currentVersion)
oracle.atVersion.whenCalledWith(currentVersion[0]).returns(currentVersion)

marketOwner = await impersonateWithBalance(await controller['owner(address)'](long.address), utils.parseEther('10'))
})

describe('#initialize', () => {
Expand Down Expand Up @@ -837,6 +847,82 @@ describe('SingleBalancedVault', () => {
expect(await shortPosition()).to.equal(0)
})

it('closes 0 value position if above maker limit', async () => {
// Get maker product very close to the makerLimit
await asset.connect(perennialUser).approve(collateral.address, constants.MaxUint256)
await collateral
.connect(perennialUser)
.depositTo(perennialUser.address, short.address, utils.parseEther('400000'))
const shortMakerAvailable = (await short.makerLimit()).sub(
(await short.positionAtVersion(await short['latestVersion()']())).maker,
)
await collateral.connect(perennialUser).depositTo(perennialUser.address, long.address, utils.parseEther('400000'))
const longMakerAvailable = (await long.makerLimit()).sub(
(await long.positionAtVersion(await long['latestVersion()']())).maker,
)

await short.connect(perennialUser).openMake(shortMakerAvailable)
await long.connect(perennialUser).openMake(longMakerAvailable)
await updateOracle()
await vault.sync()

await short.connect(marketOwner).updateMakerLimit(0)
await long.connect(marketOwner).updateMakerLimit(0)

// Deposit should create a greater position than what's available
const largeDeposit = utils.parseEther('10000')
await vault.connect(user).deposit(largeDeposit, user.address)
await updateOracle()
await expect(vault.sync())
.to.emit(short, 'MakeClosed')
.withArgs(vault.address, anyUint, 0)
.to.emit(long, 'MakeClosed')
.withArgs(vault.address, anyUint, 0)

expect(await longPosition()).to.equal(0)
expect(await shortPosition()).to.equal(0)
})

it('opens 0 value position if in >=100% utilization', async () => {
// Deposit should create a greater position than what's available
const largeDeposit = utils.parseEther('10000')
await vault.connect(user).deposit(largeDeposit, user.address)
await updateOracle()
await vault.sync()

// Get taker product very close to the maker
await asset.connect(perennialUser).approve(collateral.address, constants.MaxUint256)
await collateral
.connect(perennialUser)
.depositTo(perennialUser.address, short.address, utils.parseEther('1000000'))

await short.settle()
const globalShort = await short.positionAtVersion(await short['latestVersion()']())
const shortAvailable = globalShort.maker.sub(globalShort.taker)
await short.connect(perennialUser).openTake(shortAvailable)
await updateOracle()
await short.settle()

// Increase the price to result in a profit for the vault on the short side, resulting in a close attempt on the next
// update
await updateOracle(originalOraclePrice.add(utils.parseEther('10')))
await vault.sync()
await vault.connect(user).redeem(await vault.maxRedeem(user.address), user.address)
await expect(vault.sync()).to.emit(short, 'MakeOpened').withArgs(vault.address, anyUint, 0)
})

it('opens a 0 value position if account position is at target', async () => {
// Deposit should create a greater position than what's available
const largeDeposit = utils.parseEther('10000')
await vault.connect(user).deposit(largeDeposit, user.address)
await updateOracle()
await vault.sync()

await updateOracle()
await vault.connect(user).deposit(0, user.address)
await expect(vault.sync()).to.emit(short, 'MakeOpened').withArgs(vault.address, anyUint, 0)
})

context('liquidation', () => {
context('long', () => {
it('recovers before being liquidated', async () => {
Expand Down

0 comments on commit 6ead0cc

Please sign in to comment.