Skip to content

Commit

Permalink
Merge pull request #328 from tranchess/dev-bc-fix-price-crushing
Browse files Browse the repository at this point in the history
Split QUEEN in oracle proxy
  • Loading branch information
bill-clippy authored Jan 16, 2024
2 parents 2efbda8 + b5ab6f0 commit 45aaddc
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 80 deletions.
30 changes: 16 additions & 14 deletions contracts/oracle/BscAprOracleProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity >=0.6.10 <0.8.0;

import "../interfaces/IAprOracle.sol";
import "../interfaces/IFundV3.sol";
import "../interfaces/IPrimaryMarketV3.sol";
import "../interfaces/ITrancheIndexV2.sol";
import "../fund/ShareStaking.sol";

Expand All @@ -28,6 +29,8 @@ import "../fund/ShareStaking.sol";
// benefits, which is basically next to zero.

contract BscAprOracleProxy is IAprOracle, ITrancheIndexV2 {
// Under extreme circumstances, there might not be enough amount of token to deposit;
// we could always transfer more QUEEN to resolve the issue.
uint256 public constant DEPOSIT_AMOUNT = 1e15;
IAprOracle public immutable aprOracle;
IFundV3 public immutable fund;
Expand All @@ -40,25 +43,18 @@ contract BscAprOracleProxy is IAprOracle, ITrancheIndexV2 {
fund = fund_;
shareStaking = shareStaking_;
currentVersion = fund_.getRebalanceSize();

// Approve max BISHOP and ROOK to ShareStaking
fund_.trancheApprove(
TRANCHE_B,
address(shareStaking_),
type(uint256).max,
fund_.getRebalanceSize()
);
fund_.trancheApprove(
TRANCHE_R,
address(shareStaking_),
type(uint256).max,
fund_.getRebalanceSize()
);
_approveMax(fund_, address(shareStaking_));
}

function capture() external override returns (uint256 dailyRate) {
uint256 newVersion = fund.getRebalanceSize();
if (newVersion != currentVersion) {
uint256 amountQ = fund.trancheBalanceOf(TRANCHE_Q, address(this));
if (amountQ > 0) {
IPrimaryMarketV3 primaryMarket = IPrimaryMarketV3(fund.primaryMarket());
primaryMarket.split(address(this), amountQ, newVersion);
_approveMax(fund, address(shareStaking));
}
currentVersion = newVersion;
uint256 oldStakingQ = shareStaking.totalSupply(TRANCHE_Q);
shareStaking.deposit(TRANCHE_B, DEPOSIT_AMOUNT, address(this), newVersion);
Expand All @@ -70,4 +66,10 @@ contract BscAprOracleProxy is IAprOracle, ITrancheIndexV2 {
}
return aprOracle.capture();
}

function _approveMax(IFundV3 fund_, address spender) private {
// Approve max BISHOP and ROOK to ShareStaking
fund_.trancheApprove(TRANCHE_B, spender, type(uint256).max, fund_.getRebalanceSize());
fund_.trancheApprove(TRANCHE_R, spender, type(uint256).max, fund_.getRebalanceSize());
}
}
102 changes: 36 additions & 66 deletions test/checkpointBypassAttack.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from "chai";
import { BigNumberish, Contract, Wallet } from "ethers";
import { Contract, Wallet } from "ethers";
import type { Fixture, MockContract, MockProvider } from "ethereum-waffle";
import { waffle, ethers } from "hardhat";
const { loadFixture } = waffle;
Expand Down Expand Up @@ -32,7 +32,7 @@ describe("checkpointBypassAttack", function () {
readonly shareQ: MockContract;
readonly shareB: MockContract;
readonly shareR: MockContract;
readonly primaryMarket: MockContract;
readonly primaryMarket: Contract;
readonly aprOracleProxy: Contract;
readonly staking: Contract;
readonly fund: Contract;
Expand All @@ -47,7 +47,7 @@ describe("checkpointBypassAttack", function () {
let owner: Wallet;
let twapOracle: MockContract;
let btc: Contract;
let primaryMarket: MockContract;
let primaryMarket: Contract;
let aprOracleProxy: Contract;
let staking: Contract;
let fund: Contract;
Expand Down Expand Up @@ -93,17 +93,19 @@ describe("checkpointBypassAttack", function () {
await share.mock.fundEmitTransfer.returns();
await share.mock.fundEmitApproval.returns();
}
const primaryMarket = await deployMockForName(owner, "IPrimaryMarketV3");
await primaryMarket.mock.settle.returns();

const primaryMarketAddress = ethers.utils.getContractAddress({
from: owner.address,
nonce: (await owner.getTransactionCount("pending")) + 1,
});
const Fund = await ethers.getContractFactory("FundV4");
const fund = await Fund.connect(owner).deploy([
btc.address,
8,
shareQ.address,
shareB.address,
shareR.address,
primaryMarket.address,
primaryMarketAddress,
ethers.constants.AddressZero,
0,
UPPER_REBALANCE_THRESHOLD,
Expand All @@ -113,6 +115,16 @@ describe("checkpointBypassAttack", function () {
interestRateBallot.address,
feeCollector.address,
]);
const PrimaryMarket = await ethers.getContractFactory("PrimaryMarketV4");
const primaryMarket = await PrimaryMarket.connect(owner).deploy(
fund.address,
0,
0,
parseEther("1000000"),
false
);
expect(primaryMarket.address).to.equal(primaryMarketAddress);

await fund.initialize(parseEther("500"), parseEther("1"), parseEther("1"), 0);

const chessSchedule = await deployMockForName(owner, "IChessSchedule");
Expand Down Expand Up @@ -150,13 +162,13 @@ describe("checkpointBypassAttack", function () {
startDay,
startTimestamp,
twapOracle,
btc,
btc: btc.connect(user1),
aprOracle,
interestRateBallot,
shareQ,
shareB,
shareR,
primaryMarket,
primaryMarket: primaryMarket.connect(user1),
aprOracleProxy,
staking: staking.connect(user1),
fund: fund.connect(user1),
Expand All @@ -168,23 +180,6 @@ describe("checkpointBypassAttack", function () {
await fund.settle();
}

async function pmCreate(
user: Wallet,
inBtc: BigNumberish,
outQ: BigNumberish,
version?: number
): Promise<void> {
await btc.connect(user).transfer(fund.address, inBtc);
await primaryMarket.call(
fund,
"primaryMarketMint",
TRANCHE_Q,
user.address,
outQ,
version ?? 0
);
}

before(function () {
currentFixture = deployFixture;
});
Expand All @@ -211,49 +206,12 @@ describe("checkpointBypassAttack", function () {
.connect(owner)
.updateDailyProtocolFeeRate(parseEther("0.0001").mul(DAILY_PROTOCOL_FEE_BPS));
// Create 10 QUEEN with 10 BTC on the first day.
await pmCreate(user1, parseBtc("10"), parseEther("10"));
await btc.transfer(primaryMarket.address, parseBtc("10"));
await primaryMarket.create(user1.address, 0, 0);
await twapOracle.mock.getTwap.withArgs(startDay).returns(parseEther("1000"));
await advanceOneDayAndSettle();
await primaryMarket.call(
fund,
"primaryMarketBurn",
TRANCHE_Q,
user1.address,
parseEther("3"),
0
);
await primaryMarket.call(
fund,
"primaryMarketMint",
TRANCHE_B,
aprOracleProxy.address,
parseEther("500"),
0
);
await primaryMarket.call(
fund,
"primaryMarketMint",
TRANCHE_R,
aprOracleProxy.address,
parseEther("500"),
0
);
await primaryMarket.call(
fund,
"primaryMarketMint",
TRANCHE_B,
user1.address,
parseEther("1000"),
0
);
await primaryMarket.call(
fund,
"primaryMarketMint",
TRANCHE_R,
user1.address,
parseEther("1000"),
0
);
await primaryMarket.split(aprOracleProxy.address, parseEther("1"), 0);
await primaryMarket.split(user1.address, parseEther("2"), 0);
await fund.trancheApprove(TRANCHE_Q, staking.address, parseEther("0.5"), 0);
await fund.trancheApprove(TRANCHE_B, staking.address, parseEther("0.5"), 0);
await fund.trancheApprove(TRANCHE_R, staking.address, parseEther("0.5"), 0);
Expand Down Expand Up @@ -309,5 +267,17 @@ describe("checkpointBypassAttack", function () {
await fund.settle();
expect(await fund.getRebalanceSize()).to.equal(1);
});

it("Should block settlement if extreme lower rebalance triggered", async function () {
const price = parseEther("250");
await twapOracle.mock.getTwap.withArgs(startDay + DAY).returns(price);
await setAutomine(false);
await staking.claimRewards(user2.address);
await setAutomine(true);
await expect(fund.settle()).to.be.revertedWith("SafeMath: subtraction overflow");
expect(await fund.getRebalanceSize()).to.equal(0);
await fund.settle();
expect(await fund.getRebalanceSize()).to.equal(1);
});
});
});

0 comments on commit 45aaddc

Please sign in to comment.