Skip to content

Commit

Permalink
feat(disputer): add single reserve currency disputer contracts to dis…
Browse files Browse the repository at this point in the history
…puter (UMAprotocol#2976)
  • Loading branch information
chrismaree authored May 19, 2021
1 parent b3f030a commit cfd4b43
Show file tree
Hide file tree
Showing 9 changed files with 832 additions and 64 deletions.
42 changes: 41 additions & 1 deletion packages/disputer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { SUPPORTED_CONTRACT_VERSIONS } = require("@uma/common");

// JS libs
const { Disputer } = require("./src/disputer");
const { ProxyTransactionWrapper } = require("./src/proxyTransactionWrapper");
const {
multicallAddressMap,
FinancialContractClient,
Expand All @@ -18,10 +19,11 @@ const {
waitForLogger,
createReferencePriceFeedForFinancialContract,
setAllowance,
DSProxyManager,
} = require("@uma/financial-templates-lib");

// Truffle contracts.
const { getAbi, findContractVersion } = require("@uma/core");
const { getAbi, findContractVersion, getAddress } = require("@uma/core");
const { getWeb3, PublicNetworks } = require("@uma/common");

/**
Expand All @@ -45,6 +47,7 @@ async function run({
priceFeedConfig,
disputerConfig,
disputerOverridePrice,
proxyTransactionWrapperConfig,
}) {
try {
const getTime = () => Math.round(new Date().getTime() / 1000);
Expand Down Expand Up @@ -145,9 +148,36 @@ async function run({
const gasEstimator = new GasEstimator(logger);
await gasEstimator.update();

let dsProxyManager;
if (proxyTransactionWrapperConfig?.useDsProxyToDispute) {
dsProxyManager = new DSProxyManager({
logger,
web3,
gasEstimator,
account: accounts[0],
dsProxyFactoryAddress:
proxyTransactionWrapperConfig?.dsProxyFactoryAddress || getAddress("DSProxyFactory", networkId),
dsProxyFactoryAbi: getAbi("DSProxyFactory"),
dsProxyAbi: getAbi("DSProxy"),
});

// Load in an existing DSProxy for the account EOA if one already exists or create a new one for the user.
await dsProxyManager.initializeDSProxy();
}

const proxyTransactionWrapper = new ProxyTransactionWrapper({
web3,
financialContract,
gasEstimator,
account: accounts[0],
dsProxyManager,
proxyTransactionWrapperConfig,
});

const disputer = new Disputer({
logger,
financialContractClient,
proxyTransactionWrapper,
gasEstimator,
priceFeed,
account: accounts[0],
Expand Down Expand Up @@ -260,6 +290,16 @@ async function Poll(callback) {
// If there is a DISPUTER_OVERRIDE_PRICE environment variable then the disputer will disregard the price from the
// price feed and preform disputes at this override price. Use with caution as wrong input could cause invalid disputes.
disputerOverridePrice: process.env.DISPUTER_OVERRIDE_PRICE,
// If there is a dsproxy config, the bot can be configured to send transactions via a smart contract wallet (DSProxy).
// This enables the bot to preform swap, dispute, enabling a single reserve currency.
// Note that the DSProxy will be deployed on the first run of the bot. Subsequent runs will re-use the proxy. example:
// { "useDsProxyToLiquidate": "true", If enabled, the bot will send liquidations via a DSProxy.
// "dsProxyFactoryAddress": "0x123..." -> Will default to an UMA deployed version if non provided.
// "disputerReserveCurrencyAddress": "0x123..." -> currency DSProxy will trade from when liquidating.
// "uniswapRouterAddress": "0x123..." -> uniswap router address to enable reserve trading. Defaults to mainnet router.
// "maxReserverTokenSpent": "10000000000"} -> max amount to spend in reserve currency. scaled by reserve currency
// decimals. defaults to MAX_UINT (no limit).
proxyTransactionWrapperConfig: process.env.DSPROXY_CONFIG ? JSON.parse(process.env.DSPROXY_CONFIG) : {},
};

await run({ logger: Logger, web3: getWeb3(), ...executionParameters });
Expand Down
60 changes: 22 additions & 38 deletions packages/disputer/src/disputer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Disputer {
* @notice Constructs new Disputer bot.
* @param {Object} logger Winston module used to send logs.
* @param {Object} financialContractClient Module used to query Financial Contract information on-chain.
* @param {Object} proxyTransactionWrapper Module enable the disputer to send transactions via a DSProxy.
* @param {Object} gasEstimator Module used to estimate optimal gas price with which to send txns.
* @param {Object} priceFeed Module used to get the current or historical token price.
* @param {String} account Ethereum account from which to send txns.
Expand All @@ -19,6 +20,7 @@ class Disputer {
constructor({
logger,
financialContractClient,
proxyTransactionWrapper,
gasEstimator,
priceFeed,
account,
Expand All @@ -28,6 +30,8 @@ class Disputer {
this.logger = logger;
this.account = account;

this.proxyTransactionWrapper = proxyTransactionWrapper;

// Expiring multiparty contract to read contract state
this.financialContractClient = financialContractClient;
this.web3 = this.financialContractClient.web3;
Expand Down Expand Up @@ -154,7 +158,6 @@ class Disputer {
price: price.toString(),
liquidation: JSON.stringify(liquidation),
});

return { ...liquidation, price: price.toString() };
}

Expand All @@ -172,54 +175,35 @@ class Disputer {
}

for (const disputeableLiquidation of disputableLiquidationsWithPrices) {
// Create the transaction.
const dispute = this.financialContract.methods.dispute(disputeableLiquidation.id, disputeableLiquidation.sponsor);

this.logger.debug({
at: "Disputer",
message: "Disputing liquidation",
liquidation: disputeableLiquidation,
});
try {
// Get successful transaction receipt and return value or error.
const transactionResult = await runTransaction({
transaction: dispute,
config: {
gasPrice: this.gasEstimator.getCurrentFastPrice(),
from: this.account,
nonce: await this.web3.eth.getTransactionCount(this.account),
},
});
const receipt = transactionResult.receipt;
const returnValue = transactionResult.returnValue.toString();
const logResult = {
tx: receipt.transactionHash,
sponsor: receipt.events.LiquidationDisputed.returnValues.sponsor,
liquidator: receipt.events.LiquidationDisputed.returnValues.liquidator,
id: receipt.events.LiquidationDisputed.returnValues.liquidationId,
disputeBondPaid: receipt.events.LiquidationDisputed.returnValues.disputeBondAmount,
};
this.logger.info({

// Submit the dispute transaction. This will use the DSProxy if configured or will send the tx with the unlocked EOA.
const logResult = await this.proxyTransactionWrapper.submitDisputeTransaction([
disputeableLiquidation.id,
disputeableLiquidation.sponsor,
]);

if (logResult instanceof Error || !logResult)
this.logger.error({
at: "Disputer",
message: "Position has been disputed!👮‍♂️",
message:
logResult.type === "call"
? "Cannot dispute liquidation: not enough collateral (or large enough approval) to initiate dispute✋"
: "Failed to dispute liquidation🚨",
liquidation: disputeableLiquidation,
disputeResult: logResult,
totalPaid: returnValue,
logResult,
});
} catch (error) {
const message =
error.type === "call"
? "Cannot dispute liquidation: not enough collateral (or large enough approval) to initiate dispute✋"
: "Failed to dispute liquidation🚨";
this.logger.error({
else
this.logger.info({
at: "Disputer",
message,
disputer: this.account,
message: "Liquidation has been disputed!👮‍♂️",
liquidation: disputeableLiquidation,
error,
logResult,
});
continue;
}
}
}

Expand Down
183 changes: 183 additions & 0 deletions packages/disputer/src/proxyTransactionWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
const assert = require("assert");

const { createObjectFromDefaultProps, runTransaction, blockUntilBlockMined, MAX_UINT_VAL } = require("@uma/common");
const { getAbi, getTruffleContract } = require("@uma/core");

class ProxyTransactionWrapper {
/**
* @notice Constructs new ProxyTransactionWrapper. This adds support DSProxy atomic dispute support to the bots.
* @param {Object} web3 Provider from Truffle instance to connect to Ethereum network.
* @param {Object} financialContract instance of a financial contract. Either a EMP or a perp. Used to send disputes.
* @param {Object} gasEstimator Module used to estimate optimal gas price with which to send txns.
* @param {String} account Ethereum account from which to send txns.
* @param {Object} dsProxyManager Module to send transactions via DSProxy. If null will use the unlocked account EOA.
* @param {Boolean} useDsProxyToDispute Toggles the mode Disputes will be sent with. If true then then Disputes.
* are sent from the DSProxy. Else, Transactions are sent from the EOA. If true dsProxyManager must not be null.
* @param {Object} proxyTransactionWrapperConfig configuration object used to paramaterize how the DSProxy is used. Expected:
* { uniswapRouterAddress: 0x123..., // uniswap router address. Defaults to mainnet router
disputerReserveCurrencyAddress: 0x123... // address of the reserve currency for the bot to trade against
maxReserverTokenSpent: "10000" // define the maximum amount of reserve currency the bot should use in 1tx. }
* */
constructor({
web3,
financialContract,
gasEstimator,
account,
dsProxyManager = undefined,
proxyTransactionWrapperConfig,
}) {
this.web3 = web3;
this.financialContract = financialContract;
this.gasEstimator = gasEstimator;
this.account = account;
this.dsProxyManager = dsProxyManager;

// Helper functions from web3.
this.toBN = this.web3.utils.toBN;
this.toWei = this.web3.utils.toWei;
this.toChecksumAddress = this.web3.utils.toChecksumAddress;

this.tradeDeadline = 10 * 60 * 60;

// TODO: refactor the router to pull from a constant file.
const defaultConfig = {
useDsProxyToDispute: {
value: false,
isValid: (x) => {
return typeof x == "boolean";
},
},
uniswapRouterAddress: {
value: "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
isValid: (x) => {
return this.web3.utils.isAddress(x);
},
},
disputerReserveCurrencyAddress: {
value: "",
isValid: (x) => {
return this.web3.utils.isAddress(x) || x === "";
},
},
maxReserverTokenSpent: {
value: MAX_UINT_VAL,
isValid: (x) => {
return typeof x == "string";
},
},
};

// Validate and set config settings to class state.
const configWithDefaults = createObjectFromDefaultProps(proxyTransactionWrapperConfig, defaultConfig);
Object.assign(this, configWithDefaults);

// Preform some basic initalization sanity checks.
if (this.useDsProxyToDispute) {
assert(
this.dsProxyManager && this.dsProxyManager.getDSProxyAddress(),
"DSProxy Manger has not yet been initialized!"
);
assert(this.dsProxyManager != undefined, "Cant use dsProxy to dispute if the client is set to undefined!");
assert(
this.web3.utils.isAddress(this.disputerReserveCurrencyAddress),
"Must provide a reserve currency address to use the proxy transaction wrapper!"
);
}

this.reserveToken = new this.web3.eth.Contract(getAbi("ExpandedERC20"), this.disputerReserveCurrencyAddress);
this.ReserveCurrencyDisputer = getTruffleContract("ReserveCurrencyDisputer", this.web3);
}

// Main entry point for submitting a dispute. If the bot is not using a DSProxy then simply send a normal EOA tx.
// If the bot is using a DSProxy then route the tx via it.
async submitDisputeTransaction(disputeArgs) {
// If the liquidator is not using a DSProxy, use the old method of liquidating
if (!this.useDsProxyToDispute) return await this._executeDisputeWithoutDsProxy(disputeArgs);
else return await this._executeDisputeWithDsProxy(disputeArgs);
}

async _executeDisputeWithoutDsProxy(disputeArgs) {
// Dispute strategy will control how much to liquidate
// Create the transaction.
const dispute = this.financialContract.methods.dispute(...disputeArgs);

// Send the transaction or report failure.
let receipt, returnValue;
try {
// Get successful transaction receipt and return value or error.
const transactionResult = await runTransaction({
transaction: dispute,
config: {
gasPrice: this.gasEstimator.getCurrentFastPrice(),
from: this.account,
nonce: await this.web3.eth.getTransactionCount(this.account),
},
});
receipt = transactionResult.receipt;
returnValue = transactionResult.returnValue.toString();
} catch (error) {
return error;
}

return {
type: "Standard EOA Dispute",
tx: receipt && receipt.transactionHash,
sponsor: receipt.events.LiquidationDisputed.returnValues.sponsor,
liquidator: receipt.events.LiquidationDisputed.returnValues.liquidator,
id: receipt.events.LiquidationDisputed.returnValues.liquidationId,
disputeBondPaid: receipt.events.LiquidationDisputed.returnValues.disputeBondAmount,
totalPaid: returnValue,
txnConfig: {
gasPrice: this.gasEstimator.getCurrentFastPrice(),
from: this.account,
},
};
}

async _executeDisputeWithDsProxy(disputeArgs) {
const reserveCurrencyDisputer = new this.web3.eth.Contract(this.ReserveCurrencyDisputer.abi);

const callData = reserveCurrencyDisputer.methods
.swapDispute(
this.uniswapRouterAddress, // uniswapRouter
this.financialContract._address, // financialContract
this.reserveToken._address, // reserveCurrency
disputeArgs[0], // liquidationId
disputeArgs[1], // sponsor
this.maxReserverTokenSpent, // maxReserverTokenSpent
Number((await this.web3.eth.getBlock("latest")).timestamp) + this.tradeDeadline
)
.encodeABI();
const callCode = this.ReserveCurrencyDisputer.bytecode;

const dsProxyCallReturn = await this.dsProxyManager.callFunctionOnNewlyDeployedLibrary(callCode, callData);
const blockAfterDispute = await this.web3.eth.getBlockNumber();

// Wait exactly one block to fetch events. This ensures that the events have been indexed by your node.
await blockUntilBlockMined(this.web3, blockAfterDispute + 1);

const DisputeEvent = (
await this.financialContract.getPastEvents("LiquidationDisputed", {
fromBlock: blockAfterDispute - 1,
filter: { disputer: this.dsProxyManager.getDSProxyAddress() },
})
)[0];

// Return the same data sent back from the EOA Dispute.
return {
type: "DSProxy Swap and dispute transaction",
tx: dsProxyCallReturn.transactionHash,
sponsor: DisputeEvent.returnValues.sponsor,
liquidator: DisputeEvent.returnValues.liquidator,
disputer: DisputeEvent.returnValues.disputer,
liquidationId: DisputeEvent.returnValues.liquidationId,
disputeBondAmount: DisputeEvent.returnValues.disputeBondAmount,
txnConfig: {
from: dsProxyCallReturn.from,
gas: dsProxyCallReturn.gasUsed,
},
};
}
}

module.exports = { ProxyTransactionWrapper };
Loading

0 comments on commit cfd4b43

Please sign in to comment.