Skip to content

Commit

Permalink
Detect leverage in subgraph (#626)
Browse files Browse the repository at this point in the history
- Subgraph: reorganize the operations handling + fix the leverage detection
- openLeveragePosition tx flow: don’t store the leverage mode in storage (rely on the subgraph instead)
  • Loading branch information
bpierre authored Dec 2, 2024
1 parent 08a7d2e commit 4d89832
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 125 deletions.
10 changes: 1 addition & 9 deletions frontend/app/src/tx-flows/openLeveragePosition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export const openLeveragePosition: FlowDeclaration<Request, Step> = {
throw new Error("Not implemented");
},

async postFlowCheck({ request, steps, storedState }) {
async postFlowCheck({ request, steps }) {
const lastStep = steps?.at(-1);

if (lastStep?.txStatus !== "post-check" || !isTroveId(lastStep.txReceiptData)) {
Expand All @@ -316,14 +316,6 @@ export const openLeveragePosition: FlowDeclaration<Request, Step> = {
while (true) {
const { trove } = await graphQuery(TroveByIdQuery, { id: prefixedTroveId });
if (trove !== null) {
storedState.setState(({ loanModes }) => {
return {
loanModes: {
...loanModes,
[prefixedTroveId]: "leverage",
},
};
});
return;
}
}
Expand Down
274 changes: 158 additions & 116 deletions subgraph/src/TroveManager.mapping.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Address, BigInt, ByteArray, crypto, dataSource } from "@graphprotocol/graph-ts";
import { Address, BigInt, Bytes, dataSource } from "@graphprotocol/graph-ts";
import { BorrowerInfo, Collateral, InterestBatch, InterestRateBracket, Trove } from "../generated/schema";
import {
BatchUpdated as BatchUpdatedEvent,
Expand All @@ -7,6 +7,8 @@ import {
} from "../generated/templates/TroveManager/TroveManager";
import { TroveNFT as TroveNFTContract } from "../generated/templates/TroveManager/TroveNFT";

// decides whether to update the flag indicating
// that a trove might be leveraged or not.
enum LeverageUpdate {
yes = 0,
no = 1,
Expand All @@ -27,90 +29,88 @@ let OP_OPEN_TROVE_AND_JOIN_BATCH = 7;
let OP_SET_INTEREST_BATCH_MANAGER = 8;
let OP_REMOVE_FROM_BATCH = 9;

let FLASH_LOAN_TOPIC_HASH = crypto.keccak256(
ByteArray.fromUTF8("FlashLoan(address,address,uint256,uint256)"),
).toHexString();
let FLASH_LOAN_TOPIC = Bytes.fromHexString(
// keccak256("FlashLoan(address,address,uint256,uint256)")
"0x0d7d75e01ab95780d3cd1c8ec0dd6c2ce19e3a20427eec8bf53283b6fb8e95f0",
);

export function handleTroveOperation(event: TroveOperationEvent): void {
let timestamp = event.block.timestamp;
let troveId = event.params._troveId;
let collId = dataSource.context().getString("collId");
let troveFullId = collId + ":" + troveId.toHexString();

let operation = event.params._operation;
let tm = TroveManagerContract.bind(event.address);
let leverageUpdate = getLeverageUpdate(event);
let trove = Trove.load(troveFullId);

switch (operation) {
case OP_OPEN_TROVE:
case OP_ADJUST_TROVE:
case OP_ADJUST_TROVE_INTEREST_RATE:
case OP_APPLY_PENDING_DEBT:
updateTrove(tm, troveId, timestamp, leverageUpdate);
break;

case OP_OPEN_TROVE_AND_JOIN_BATCH:
updateTrove(tm, troveId, timestamp, leverageUpdate);
enterBatch(collId, troveId, tm.Troves(troveId).getInterestBatchManager());
break;

case OP_SET_INTEREST_BATCH_MANAGER:
enterBatch(collId, troveId, tm.Troves(troveId).getInterestBatchManager());
break;

case OP_REMOVE_FROM_BATCH:
leaveBatch(collId, troveId, event.params._annualInterestRate);
break;

case OP_REDEEM_COLLATERAL:
updateTrove(tm, troveId, timestamp, leverageUpdate);

if (!trove) {
throw new Error("Trove not found: " + troveFullId);
}
trove.status = "redeemed";
trove.save();
break;
let trove: Trove | null = null;

case OP_CLOSE_TROVE:
updateTrove(tm, troveId, timestamp, leverageUpdate);
if (operation === OP_OPEN_TROVE) {
updateTrove(tm, troveId, timestamp, getLeverageUpdate(event), true);
return;
}

if (!trove) {
throw new Error("Trove not found: " + troveFullId);
}
if (operation === OP_ADJUST_TROVE) {
updateTrove(tm, troveId, timestamp, getLeverageUpdate(event), true);
return;
}

if (trove.interestBatch !== null) {
leaveBatch(collId, troveId, BigInt.fromI32(0));
}
if (operation === OP_APPLY_PENDING_DEBT) {
updateTrove(tm, troveId, timestamp, LeverageUpdate.unchanged, false);
return;
}

trove.closedAt = timestamp;
trove.status = "closed";
trove.save();
break;
if (operation === OP_OPEN_TROVE_AND_JOIN_BATCH) {
updateTrove(tm, troveId, timestamp, getLeverageUpdate(event), true);
enterBatch(collId, troveId, tm.Troves(troveId).getInterestBatchManager());
return;
}

case OP_LIQUIDATE:
updateTrove(tm, troveId, timestamp, leverageUpdate);
if (operation === OP_ADJUST_TROVE_INTEREST_RATE) {
updateTrove(tm, troveId, timestamp, LeverageUpdate.unchanged, false);
return;
}

if (!trove) {
throw new Error("Trove not found: " + troveFullId);
}
if (operation === OP_SET_INTEREST_BATCH_MANAGER) {
enterBatch(collId, troveId, tm.Troves(troveId).getInterestBatchManager());
return;
}

if (trove.interestBatch !== null) {
leaveBatch(collId, troveId, BigInt.fromI32(0));
}
if (operation === OP_REMOVE_FROM_BATCH) {
leaveBatch(collId, troveId, event.params._annualInterestRate);
return;
}

trove.debt = event.params._debtIncreaseFromRedist;
trove.deposit = event.params._collIncreaseFromRedist;
if (operation === OP_REDEEM_COLLATERAL) {
trove = updateTrove(tm, troveId, timestamp, LeverageUpdate.unchanged, false);
trove.status = "redeemed";
trove.save();
return;
}

trove.closedAt = timestamp;
trove.status = "liquidated";
trove.save();
break;
if (operation === OP_CLOSE_TROVE) {
trove = updateTrove(tm, troveId, timestamp, LeverageUpdate.unchanged, false);
if (trove.interestBatch !== null) {
leaveBatch(collId, troveId, BigInt.fromI32(0));
}
trove.closedAt = timestamp;
trove.status = "closed";
trove.save();
return;
}

default:
throw new Error("Unsupported operation: " + operation.toString());
if (operation === OP_LIQUIDATE) {
trove = updateTrove(tm, troveId, timestamp, LeverageUpdate.unchanged, false);
if (trove.interestBatch !== null) {
leaveBatch(collId, troveId, BigInt.fromI32(0));
}
trove.debt = event.params._debtIncreaseFromRedist;
trove.deposit = event.params._collIncreaseFromRedist;
trove.closedAt = timestamp;
trove.status = "liquidated";
trove.save();
return;
}

throw new Error("Unsupported operation: " + operation.toString());
}

function getLeverageUpdate(event: TroveOperationEvent): LeverageUpdate {
Expand All @@ -127,7 +127,7 @@ function getLeverageUpdate(event: TroveOperationEvent): LeverageUpdate {
let receipt = event.receipt;
let logs = receipt ? receipt.logs : [];
for (let i = 0; i < logs.length; i++) {
if (logs[i].topics[0].toHexString() === FLASH_LOAN_TOPIC_HASH) {
if (logs[i].topics[0].equals(FLASH_LOAN_TOPIC)) {
return LeverageUpdate.yes;
}
}
Expand Down Expand Up @@ -259,6 +259,73 @@ function updateRateBracketDebt(
newRateBracket.save();
}

function createTrove(
troveId: BigInt,
debt: BigInt,
deposit: BigInt,
stake: BigInt,
interestRate: BigInt,
timestamp: BigInt,
mightBeLeveraged: boolean,
): Trove {
let collId = dataSource.context().getString("collId");
let troveFullId = collId + ":" + troveId.toHexString();

let collateral = Collateral.load(collId);
if (!collateral) {
throw new Error("Non-existent collateral: " + collId);
}

let trove = Trove.load(troveFullId);
if (trove) {
throw new Error("Trove already exists: " + troveFullId);
}

let borrower = TroveNFTContract.bind(Address.fromBytes(
dataSource.context().getBytes("address:troveNft"),
)).ownerOf(troveId);

// create borrower info if needed
let borrowerInfo = BorrowerInfo.load(borrower.toHexString());
if (!borrowerInfo) {
borrowerInfo = new BorrowerInfo(borrower.toHexString());
borrowerInfo.troves = 0;

let totalCollaterals = dataSource.context().getI32("totalCollaterals");
borrowerInfo.trovesByCollateral = (new Array<i32>(totalCollaterals)).fill(0);
borrowerInfo.lastTroveIdByCollateral = (new Array<i32>(totalCollaterals)).fill(0);
}

// update borrower info
let trovesByColl = borrowerInfo.trovesByCollateral;
trovesByColl[collateral.collIndex] += 1;
borrowerInfo.trovesByCollateral = trovesByColl;
borrowerInfo.troves += 1;
borrowerInfo.save();

// create trove
trove = new Trove(troveFullId);
trove.borrower = borrower;
trove.collateral = collId;
trove.createdAt = timestamp;
trove.debt = debt;
trove.deposit = deposit;
trove.stake = stake;
trove.status = "active";
trove.troveId = troveId.toHexString();
trove.updatedAt = timestamp;

// batches are handled separately, not
// when creating the trove but right after
trove.interestRate = interestRate;
trove.interestBatch = null;
trove.mightBeLeveraged = mightBeLeveraged;

trove.save();

return trove;
}

// When a trove gets updated (either on TroveUpdated or BatchedTroveUpdated):
// 1. update the collateral (branch) total deposited & debt
// 2. create the trove entity if it doesn't exist
Expand All @@ -270,8 +337,9 @@ function updateTrove(
troveManagerContract: TroveManagerContract,
troveId: BigInt,
timestamp: BigInt,
mightBeLeveragedUpdate: LeverageUpdate,
): void {
leverageUpdate: LeverageUpdate,
createIfMissing: boolean,
): Trove {
let collId = dataSource.context().getString("collId");

let collateral = Collateral.load(collId);
Expand All @@ -298,69 +366,43 @@ function updateTrove(

// create trove if needed
if (!trove) {
let borrower = TroveNFTContract.bind(Address.fromBytes(
dataSource.context().getBytes("address:troveNft"),
)).ownerOf(troveId);

// create borrower info if needed
let borrowerInfo = BorrowerInfo.load(borrower.toHexString());
if (!borrowerInfo) {
borrowerInfo = new BorrowerInfo(borrower.toHexString());
borrowerInfo.troves = 0;

let totalCollaterals = dataSource.context().getI32("totalCollaterals");
borrowerInfo.trovesByCollateral = (new Array<i32>(totalCollaterals)).fill(0);
if (!createIfMissing) {
throw new Error("Trove not found: " + troveFullId);
}
let trove = createTrove(
troveId,
newDebt,
newDeposit,
newStake,
newInterestRate,
timestamp,
leverageUpdate === LeverageUpdate.yes,
);

// update borrower info
let trovesByColl = borrowerInfo.trovesByCollateral;
trovesByColl[collateral.collIndex] += 1;
borrowerInfo.trovesByCollateral = trovesByColl;
borrowerInfo.troves += 1;
borrowerInfo.save();

// create trove
trove = new Trove(troveFullId);
trove.borrower = borrower;
trove.collateral = collId;
trove.createdAt = timestamp;
trove.debt = newDebt;
trove.deposit = newDeposit;
trove.interestRate = trove.interestBatch === null
? newInterestRate
: BigInt.fromI32(0);
trove.stake = newStake;
trove.status = "active";
trove.troveId = troveId.toHexString();
trove.updatedAt = timestamp;
trove.mightBeLeveraged = mightBeLeveragedUpdate === LeverageUpdate.yes;
trove.save();
// update interest rate brackets (no need to check if the trove
// is in a batch as this is done after calling updateTrove())
updateRateBracketDebt(collId, prevInterestRate, newInterestRate, prevDebt, newDebt);
return trove;
}

// update interest rate brackets for non-batched troves
if (trove.interestBatch === null) {
updateRateBracketDebt(
collId,
prevInterestRate,
newInterestRate,
prevDebt,
newDebt,
);
updateRateBracketDebt(collId, prevInterestRate, newInterestRate, prevDebt, newDebt);
}

trove.debt = newDebt;
trove.deposit = newDeposit;
trove.interestRate = trove.interestBatch === null
? newInterestRate
: BigInt.fromI32(0);
trove.interestRate = trove.interestBatch === null ? newInterestRate : BigInt.fromI32(0);
trove.stake = newStake;

if (mightBeLeveragedUpdate !== LeverageUpdate.unchanged) {
trove.mightBeLeveraged = mightBeLeveragedUpdate === LeverageUpdate.yes;
if (leverageUpdate !== LeverageUpdate.unchanged) {
trove.mightBeLeveraged = leverageUpdate === LeverageUpdate.yes;
}

trove.updatedAt = timestamp;
trove.save();

return trove;
}

// when a batch gets updated:
Expand Down

0 comments on commit 4d89832

Please sign in to comment.