Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split QUEEN in oracle proxy #328

Merged
merged 3 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
});
Loading