From cd0dd1ba064c3b503d6d6989a023b0e0be6939c8 Mon Sep 17 00:00:00 2001 From: Kirill Kuvshinov Date: Thu, 18 Jan 2024 13:47:14 +0300 Subject: [PATCH] feat: add a bot for token converter arbitrage --- package.json | 5 +- src/config/abis/AbstractTokenConverter.ts | 1272 +++++++++++++++++++++ src/config/abis/TokenConverterOperator.ts | 227 ++++ src/config/addresses.ts | 51 + src/config/chains.ts | 6 + src/config/clients.ts | 31 + src/converter-bot/path.ts | 35 + src/converter-bot/tokenConverterBot.ts | 116 ++ yarn.lock | 139 ++- 9 files changed, 1853 insertions(+), 29 deletions(-) create mode 100644 src/config/abis/AbstractTokenConverter.ts create mode 100644 src/config/abis/TokenConverterOperator.ts create mode 100644 src/config/addresses.ts create mode 100644 src/config/chains.ts create mode 100644 src/config/clients.ts create mode 100644 src/converter-bot/path.ts create mode 100644 src/converter-bot/tokenConverterBot.ts diff --git a/package.json b/package.json index e5aed9b3..e68c8537 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ }, "dependencies": { "@openzeppelin/contracts-upgradeable": "4.9.3", - "@venusprotocol/protocol-reserve": "1.0.0-converters.1", - "@venusprotocol/venus-protocol": "6.1.0-dev.8" + "@venusprotocol/protocol-reserve": "1.3.0-dev.1", + "@venusprotocol/venus-protocol": "6.1.0-dev.8", + "viem": "^2.1.1" } } diff --git a/src/config/abis/AbstractTokenConverter.ts b/src/config/abis/AbstractTokenConverter.ts new file mode 100644 index 00000000..0a8ff985 --- /dev/null +++ b/src/config/abis/AbstractTokenConverter.ts @@ -0,0 +1,1272 @@ +// @kkirka: This is a typescript file and not a JSON file because I wanted to keep +// the narrow type for viem (importing from JSON yields an array of garbage). I guess +// we could generate these files similarly to how it's done in the frontend repo +// (in the postinstall script), but I'd keep this up for discussion for now. + +export default [ + { + inputs: [ + { + internalType: "uint256", + name: "amountInMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountInMaxMantissa", + type: "uint256", + }, + ], + name: "AmountInHigherThanMax", + type: "error", + }, + { + inputs: [], + name: "AmountInMismatched", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountOutMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountOutMinMantissa", + type: "uint256", + }, + ], + name: "AmountOutLowerThanMinRequired", + type: "error", + }, + { + inputs: [], + name: "AmountOutMismatched", + type: "error", + }, + { + inputs: [], + name: "ConversionConfigNotEnabled", + type: "error", + }, + { + inputs: [], + name: "ConversionEnabledOnlyForPrivateConversions", + type: "error", + }, + { + inputs: [], + name: "ConversionTokensActive", + type: "error", + }, + { + inputs: [], + name: "ConversionTokensPaused", + type: "error", + }, + { + inputs: [], + name: "DeflationaryTokenNotSupported", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "incentive", + type: "uint256", + }, + { + internalType: "uint256", + name: "maxIncentive", + type: "uint256", + }, + ], + name: "IncentiveTooHigh", + type: "error", + }, + { + inputs: [], + name: "InputLengthMisMatch", + type: "error", + }, + { + inputs: [], + name: "InsufficientInputAmount", + type: "error", + }, + { + inputs: [], + name: "InsufficientOutputAmount", + type: "error", + }, + { + inputs: [], + name: "InsufficientPoolLiquidity", + type: "error", + }, + { + inputs: [], + name: "InvalidConverterNetwork", + type: "error", + }, + { + inputs: [], + name: "InvalidMinimumAmountToConvert", + type: "error", + }, + { + inputs: [], + name: "InvalidToAddress", + type: "error", + }, + { + inputs: [], + name: "InvalidTokenConfigAddresses", + type: "error", + }, + { + inputs: [], + name: "NonZeroIncentiveForPrivateConversion", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address", + }, + { + internalType: "address", + name: "calledContract", + type: "address", + }, + { + internalType: "string", + name: "methodSignature", + type: "string", + }, + ], + name: "Unauthorized", + type: "error", + }, + { + inputs: [], + name: "ZeroAddressNotAllowed", + type: "error", + }, + { + inputs: [], + name: "ZeroValueNotAllowed", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "oldIncentive", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "newIncentive", + type: "uint256", + }, + { + indexed: false, + internalType: "enum IAbstractTokenConverter.ConversionAccessibility", + name: "oldAccess", + type: "uint8", + }, + { + indexed: false, + internalType: "enum IAbstractTokenConverter.ConversionAccessibility", + name: "newAccess", + type: "uint8", + }, + ], + name: "ConversionConfigUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "ConversionPaused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "ConversionResumed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "receiver", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amountOut", + type: "uint256", + }, + ], + name: "ConvertedExactTokens", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "receiver", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amountOut", + type: "uint256", + }, + ], + name: "ConvertedExactTokensSupportingFeeOnTransferTokens", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "receiver", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amountOut", + type: "uint256", + }, + ], + name: "ConvertedForExactTokens", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "receiver", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amountOut", + type: "uint256", + }, + ], + name: "ConvertedForExactTokensSupportingFeeOnTransferTokens", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldConverterNetwork", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "converterNetwork", + type: "address", + }, + ], + name: "ConverterNetworkAddressUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldDestinationAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "destinationAddress", + type: "address", + }, + ], + name: "DestinationAddressUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint8", + name: "version", + type: "uint8", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "oldMinAmountToConvert", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "newMinAmountToConvert", + type: "uint256", + }, + ], + name: "MinAmountToConvertUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "oldAccessControlManager", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "newAccessControlManager", + type: "address", + }, + ], + name: "NewAccessControlManager", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferStarted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "contract ResilientOracle", + name: "oldPriceOracle", + type: "address", + }, + { + indexed: true, + internalType: "contract ResilientOracle", + name: "priceOracle", + type: "address", + }, + ], + name: "PriceOracleUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "SweepToken", + type: "event", + }, + { + inputs: [], + name: "MAX_INCENTIVE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "acceptOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "accessControlManager", + outputs: [ + { + internalType: "contract IAccessControlManagerV8", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "token", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "tokenBalance", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "conversionConfigurations", + outputs: [ + { + internalType: "uint256", + name: "incentive", + type: "uint256", + }, + { + internalType: "enum IAbstractTokenConverter.ConversionAccessibility", + name: "conversionAccess", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "conversionPaused", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountInMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountOutMinMantissa", + type: "uint256", + }, + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + ], + name: "convertExactTokens", + outputs: [ + { + internalType: "uint256", + name: "actualAmountIn", + type: "uint256", + }, + { + internalType: "uint256", + name: "actualAmountOut", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountInMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountOutMinMantissa", + type: "uint256", + }, + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + ], + name: "convertExactTokensSupportingFeeOnTransferTokens", + outputs: [ + { + internalType: "uint256", + name: "actualAmountIn", + type: "uint256", + }, + { + internalType: "uint256", + name: "actualAmountOut", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountInMaxMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountOutMantissa", + type: "uint256", + }, + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + ], + name: "convertForExactTokens", + outputs: [ + { + internalType: "uint256", + name: "actualAmountIn", + type: "uint256", + }, + { + internalType: "uint256", + name: "actualAmountOut", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountInMaxMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountOutMantissa", + type: "uint256", + }, + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + ], + name: "convertForExactTokensSupportingFeeOnTransferTokens", + outputs: [ + { + internalType: "uint256", + name: "actualAmountIn", + type: "uint256", + }, + { + internalType: "uint256", + name: "actualAmountOut", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "converterNetwork", + outputs: [ + { + internalType: "contract IConverterNetwork", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "destinationAddress", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountOutMantissa", + type: "uint256", + }, + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + ], + name: "getAmountIn", + outputs: [ + { + internalType: "uint256", + name: "amountConvertedMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountInMantissa", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountInMantissa", + type: "uint256", + }, + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + ], + name: "getAmountOut", + outputs: [ + { + internalType: "uint256", + name: "amountConvertedMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountOutMantissa", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountOutMantissa", + type: "uint256", + }, + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + ], + name: "getUpdatedAmountIn", + outputs: [ + { + internalType: "uint256", + name: "amountConvertedMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountInMantissa", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amountInMantissa", + type: "uint256", + }, + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + ], + name: "getUpdatedAmountOut", + outputs: [ + { + internalType: "uint256", + name: "amountConvertedMantissa", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountOutMantissa", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "minAmountToConvert", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pauseConversion", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "pendingOwner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "priceOracle", + outputs: [ + { + internalType: "contract ResilientOracle", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "resumeConversion", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "accessControlManager_", + type: "address", + }, + ], + name: "setAccessControlManager", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address", + name: "tokenAddressOut", + type: "address", + }, + { + components: [ + { + internalType: "uint256", + name: "incentive", + type: "uint256", + }, + { + internalType: "enum IAbstractTokenConverter.ConversionAccessibility", + name: "conversionAccess", + type: "uint8", + }, + ], + internalType: "struct IAbstractTokenConverter.ConversionConfig", + name: "conversionConfig", + type: "tuple", + }, + ], + name: "setConversionConfig", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "tokenAddressIn", + type: "address", + }, + { + internalType: "address[]", + name: "tokenAddressesOut", + type: "address[]", + }, + { + components: [ + { + internalType: "uint256", + name: "incentive", + type: "uint256", + }, + { + internalType: "enum IAbstractTokenConverter.ConversionAccessibility", + name: "conversionAccess", + type: "uint8", + }, + ], + internalType: "struct IAbstractTokenConverter.ConversionConfig[]", + name: "conversionConfigs", + type: "tuple[]", + }, + ], + name: "setConversionConfigs", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IConverterNetwork", + name: "converterNetwork_", + type: "address", + }, + ], + name: "setConverterNetwork", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "destinationAddress_", + type: "address", + }, + ], + name: "setDestination", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "minAmountToConvert_", + type: "uint256", + }, + ], + name: "setMinAmountToConvert", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract ResilientOracle", + name: "priceOracle_", + type: "address", + }, + ], + name: "setPriceOracle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "tokenAddress", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "sweepToken", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "comptroller", + type: "address", + }, + { + internalType: "address", + name: "asset", + type: "address", + }, + ], + name: "updateAssetsState", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/src/config/abis/TokenConverterOperator.ts b/src/config/abis/TokenConverterOperator.ts new file mode 100644 index 00000000..a51f5644 --- /dev/null +++ b/src/config/abis/TokenConverterOperator.ts @@ -0,0 +1,227 @@ +// @kkirka: This is a typescript file and not a JSON file because I wanted to keep +// the narrow type for viem (importing from JSON yields an array of garbage). I guess +// we could generate these files similarly to how it's done in the frontend repo +// (in the postinstall script), but I'd keep this up for discussion for now. + +export default [ + { + inputs: [ + { + internalType: "contract ISmartRouter", + name: "swapRouter_", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "ApproveFailed", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "currentTimestamp", + type: "uint256", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + ], + name: "DeadlinePassed", + type: "error", + }, + { + inputs: [], + name: "EmptySwap", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "expected", + type: "uint256", + }, + { + internalType: "uint256", + name: "actual", + type: "uint256", + }, + ], + name: "InsufficientLiquidity", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "expected", + type: "address", + }, + { + internalType: "address", + name: "actual", + type: "address", + }, + ], + name: "InvalidCallbackSender", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "expected", + type: "address", + }, + { + internalType: "address", + name: "actual", + type: "address", + }, + ], + name: "InvalidSwapEnd", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "expected", + type: "address", + }, + { + internalType: "address", + name: "actual", + type: "address", + }, + ], + name: "InvalidSwapStart", + type: "error", + }, + { + inputs: [], + name: "Overflow", + type: "error", + }, + { + inputs: [], + name: "Underflow", + type: "error", + }, + { + inputs: [], + name: "ZeroAddressNotAllowed", + type: "error", + }, + { + inputs: [], + name: "DEPLOYER", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SWAP_ROUTER", + outputs: [ + { + internalType: "contract ISmartRouter", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "beneficiary", + type: "address", + }, + { + internalType: "contract IERC20", + name: "tokenToReceiveFromConverter", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "int256", + name: "minIncome", + type: "int256", + }, + { + internalType: "contract IERC20", + name: "tokenToSendToConverter", + type: "address", + }, + { + internalType: "contract IAbstractTokenConverter", + name: "converter", + type: "address", + }, + { + internalType: "bytes", + name: "path", + type: "bytes", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + ], + internalType: "struct TokenConverterOperator.ConversionParameters", + name: "params", + type: "tuple", + }, + ], + name: "convert", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "int256", + name: "amount0Delta", + type: "int256", + }, + { + internalType: "int256", + name: "amount1Delta", + type: "int256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "pancakeV3SwapCallback", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/src/config/addresses.ts b/src/config/addresses.ts new file mode 100644 index 00000000..2029f11c --- /dev/null +++ b/src/config/addresses.ts @@ -0,0 +1,51 @@ +import bscmainnetGovernance from "@venusprotocol/governance-contracts/deployments/bscmainnet_addresses.json"; +import bsctestnetGovernance from "@venusprotocol/governance-contracts/deployments/bsctestnet_addresses.json"; +import bscmainnetProtocolReserve from "@venusprotocol/protocol-reserve/deployments/bscmainnet_addresses.json"; +import bsctestnetProtocolReserve from "@venusprotocol/protocol-reserve/deployments/bsctestnet_addresses.json"; +import bscmainnetCore from "@venusprotocol/venus-protocol/deployments/bscmainnet_addresses.json"; +import bsctestnetCore from "@venusprotocol/venus-protocol/deployments/bsctestnet_addresses.json"; + +export const addresses = { + // TODO: Replace hardcoded addresses with addresses from the packages + bscmainnet: { + ...bscmainnetCore.addresses, + ...bscmainnetProtocolReserve.addresses, + ...bscmainnetGovernance.addresses, + BUSD: "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", + USDT: "0x55d398326f99059fF775485246999027B3197955", + USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", + PancakeSwapRouter: "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4", + + BTCBPrimeConverter: "0xE8CeAa79f082768f99266dFd208d665d2Dd18f53", + ConverterNetwork: "0xF7Caad5CeB0209165f2dFE71c92aDe14d0F15995", + ETHPrimeConverter: "0xca430B8A97Ea918fF634162acb0b731445B8195E", + RiskFundConverter: "0xA5622D276CcbB8d9BBE3D1ffd1BB11a0032E53F0", + USDCPrimeConverter: "0xa758c9C215B6c4198F0a0e3FA46395Fa15Db691b", + USDTPrimeConverter: "0xD9f101AA67F3D72662609a2703387242452078C3", + XVSVaultConverter: "0xd5b9AE835F4C59272032B3B954417179573331E0", + }, + bsctestnet: { + ...bsctestnetCore.addresses, + ...bsctestnetProtocolReserve.addresses, + ...bsctestnetGovernance.addresses, + PancakeSwapRouter: "0x1b81D678ffb9C0263b24A97847620C99d213eB14", + xvsHolder: "0x2Ce1d0ffD7E869D9DF33e28552b12DdDed326706", + usdtHolder: "0x2Ce1d0ffD7E869D9DF33e28552b12DdDed326706", + + BTCBPrimeConverter: "0x989A1993C023a45DA141928921C0dE8fD123b7d1", + ETHPrimeConverter: "0xf358650A007aa12ecC8dac08CF8929Be7f72A4D9", + RiskFundConverter: "0x32Fbf7bBbd79355B86741E3181ef8c1D9bD309Bb", + USDCPrimeConverter: "0x2ecEdE6989d8646c992344fF6C97c72a3f811A13", + USDTPrimeConverter: "0xf1FA230D25fC5D6CAfe87C5A6F9e1B17Bc6F194E", + XVSVaultConverter: "0x258f49254C758a0E37DAb148ADDAEA851F4b02a2", + + TokenConverterOperator: "0x9222F8b71603318d5EEbBf0074c2Da07fEbbB9eb", + }, +} as const; + +type Addresses = typeof addresses; + +export type HasAddressFor = { + [ChainT in keyof Addresses]: Addresses[ChainT] extends Record ? ChainT : never; +}[keyof Addresses]; diff --git a/src/config/chains.ts b/src/config/chains.ts new file mode 100644 index 00000000..9a7edfe6 --- /dev/null +++ b/src/config/chains.ts @@ -0,0 +1,6 @@ +import { bsc, bscTestnet } from "viem/chains"; + +export const chains = { + bscmainnet: bsc, + bsctestnet: bscTestnet, +} as const; diff --git a/src/config/clients.ts b/src/config/clients.ts new file mode 100644 index 00000000..aac88738 --- /dev/null +++ b/src/config/clients.ts @@ -0,0 +1,31 @@ +import { HttpTransport, PublicClient, WalletClient, createPublicClient, createWalletClient, http } from "viem"; +import { PrivateKeyAccount, privateKeyToAccount } from "viem/accounts"; + +import { chains } from "./chains"; + +export const getPublicClient = ( + chainName: ChainT, +): PublicClient => { + return createPublicClient({ + chain: chains[chainName], + transport: http(process.env[`LIVE_NETWORK_${chainName}`]), + }); +}; + +const readPrivateKeyFromEnv = (chainName: string): PrivateKeyAccount => { + const key = process.env[`PRIVATE_KEY_${chainName}`]; + if (key?.startsWith("0x")) { + return privateKeyToAccount(key as `0x${string}`); + } + throw new Error(`Invalid private key for ${chainName}. Please specify PRIVATE_KEY_${chainName} env variable.`); +}; + +export const getWalletClient = ( + chainName: ChainT, +): WalletClient => { + return createWalletClient({ + chain: chains[chainName], + transport: http(process.env[`LIVE_NETWORK_${chainName}`]), + account: readPrivateKeyFromEnv(chainName), + }); +}; diff --git a/src/converter-bot/path.ts b/src/converter-bot/path.ts new file mode 100644 index 00000000..693cfd7a --- /dev/null +++ b/src/converter-bot/path.ts @@ -0,0 +1,35 @@ +import { Address, Hex, concat, numberToHex } from "viem"; + +const ensureAddress = (addressOrNumber: Address | number): Address => { + if (typeof addressOrNumber === "number") { + throw new Error(`Invalid address: ${addressOrNumber}`); + } + return addressOrNumber; +}; + +const ensureNumber = (addressOrNumber: Address | number): number => { + if (typeof addressOrNumber !== "number") { + throw new Error(`Invalid number: ${addressOrNumber}`); + } + return addressOrNumber; +}; + +export interface Path { + readonly start: Address; + readonly end: Address; + readonly data: ReadonlyArray
; + readonly hex: Hex; +} + +export const parsePath = (data: Array
): Path => { + const start = ensureAddress(data[0]); + const end = ensureAddress(data[data.length - 1]); + let hex: Hex = "0x"; + for (let i = 0; i < data.length; i += 3) { + const tokenA = ensureAddress(data[i]); + const fee = numberToHex(ensureNumber(data[i + 1]), { size: 3 }); + const tokenB = ensureAddress(data[i + 2]); + hex = concat([hex, tokenA, fee, tokenB]); + } + return { start, end, data, hex }; +}; diff --git a/src/converter-bot/tokenConverterBot.ts b/src/converter-bot/tokenConverterBot.ts new file mode 100644 index 00000000..d71cda4b --- /dev/null +++ b/src/converter-bot/tokenConverterBot.ts @@ -0,0 +1,116 @@ +import { Address, parseAbi, parseUnits } from "viem"; + +import TokenConverterOperator from "../config/abis/TokenConverterOperator"; +import { type HasAddressFor, addresses } from "../config/addresses"; +import { chains } from "../config/chains"; +import { getPublicClient, getWalletClient } from "../config/clients"; +import { Path, parsePath } from "./path"; + +type SupportedConverters = + | "BTCBPrimeConverter" + | "ETHPrimeConverter" + | "RiskFundConverter" + | "USDCPrimeConverter" + | "USDTPrimeConverter" + | "XVSVaultConverter"; + +type SupportedChains = HasAddressFor<"TokenConverterOperator" | SupportedConverters>; + +const REVERT_IF_NOT_MINED_AFTER = 60n; //seconds + +class Bot { + private chainName: SupportedChains; + private operator: { address: Address; abi: typeof TokenConverterOperator }; + private addresses: typeof addresses[SupportedChains]; + private _walletClient?: ReturnType>; + private _publicClient?: ReturnType>; + + constructor(chainName: SupportedChains) { + this.chainName = chainName; + this.addresses = addresses[chainName]; + this.operator = { + address: addresses[chainName].TokenConverterOperator, + abi: TokenConverterOperator, + }; + } + + get publicClient() { + return this._publicClient ?? getPublicClient(this.chainName); + } + + get walletClient() { + return this._walletClient ?? getWalletClient(this.chainName); + } + + async sanityCheck() { + const expected = this.addresses.PancakeSwapRouter; + const actual = await this.publicClient.readContract({ + ...this.operator, + functionName: "SWAP_ROUTER", + }); + if (expected !== actual) { + throw new Error(`Expected swap router to be at ${expected} but found at ${actual}`); + } + } + + async arbitrage(converter: SupportedConverters, path: Path, amount: bigint, minIncome: bigint) { + const converterAddress = this.addresses[converter]; + const beneficiary = this.walletClient.account.address; + const chain = chains[this.chainName]; + + if (minIncome < 0n) { + await this.walletClient.writeContract({ + address: path.end, + chain, + abi: parseAbi(["function approve(address,uint256)"]), + functionName: "approve", + args: [this.operator.address, -minIncome], + }); + } + + const block = await this.publicClient.getBlock(); + await this.walletClient.writeContract({ + ...this.operator, + chain, + functionName: "convert", + args: [ + { + beneficiary, + tokenToReceiveFromConverter: path.end, + amount, + minIncome, + tokenToSendToConverter: path.start, + converter: converterAddress, + path: path.hex, + deadline: block.timestamp + REVERT_IF_NOT_MINED_AFTER, + }, + ], + }); + } +} + +const main = async () => { + const bot = new Bot("bsctestnet"); + await bot.sanityCheck(); + + // Imagine the converter has LTC and wants USDT + // tokenToSendToConverter: USDT + // tokenToReceiveFromConverter: LTC + // We're swapping LTC to USDT on PCS, so + // the PCS reversed path should start with + // USDT (tokenToSendToConverter) and end + // with LTC (tokenToReceiveFromConverter) + // + // The income is paid out in LTC (if any) + await bot.arbitrage( + "RiskFundConverter", + parsePath([addresses.bsctestnet.USDT as Address, 500, addresses.bsctestnet.LTC as Address]), + parseUnits("1", 18), // 1 LTC + parseUnits("-1", 18), // We're ok with paying 0.1 LTC for this conversion + ); +}; + +main().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/yarn.lock b/yarn.lock index 071f977d..d5dbb842 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:1.10.0": + version: 1.10.0 + resolution: "@adraffy/ens-normalize@npm:1.10.0" + checksum: af0540f963a2632da2bbc37e36ea6593dcfc607b937857133791781e246d47f870d5e3d21fa70d5cfe94e772c284588c81ea3f5b7f4ea8fbb824369444e4dbcb + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.1.0": version: 2.2.1 resolution: "@ampproject/remapping@npm:2.2.1" @@ -1252,6 +1259,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.2.0, @noble/curves@npm:~1.2.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": 1.3.2 + checksum: bb798d7a66d8e43789e93bc3c2ddff91a1e19fdb79a99b86cd98f1e5eff0ee2024a2672902c2576ef3577b6f282f3b5c778bebd55761ddbb30e36bf275e83dd0 + languageName: node + linkType: hard + "@noble/hashes@npm:1.2.0, @noble/hashes@npm:~1.2.0": version: 1.2.0 resolution: "@noble/hashes@npm:1.2.0" @@ -1266,13 +1282,20 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": +"@noble/hashes@npm:1.3.2, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": version: 1.3.2 resolution: "@noble/hashes@npm:1.3.2" checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 languageName: node linkType: hard +"@noble/hashes@npm:~1.3.2": + version: 1.3.3 + resolution: "@noble/hashes@npm:1.3.3" + checksum: 8a6496d1c0c64797339bc694ad06cdfaa0f9e56cd0c3f68ae3666cfb153a791a55deb0af9c653c7ed2db64d537aa3e3054629740d2f2338bb1dcb7ab60cd205b + languageName: node + linkType: hard + "@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:~1.7.0": version: 1.7.1 resolution: "@noble/secp256k1@npm:1.7.1" @@ -2273,6 +2296,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:~1.1.2": + version: 1.1.5 + resolution: "@scure/base@npm:1.1.5" + checksum: 9e9ee6088cb3aa0fb91f5a48497d26682c7829df3019b1251d088d166d7a8c0f941c68aaa8e7b96bbad20c71eb210397cb1099062cde3e29d4bad6b975c18519 + languageName: node + linkType: hard + "@scure/bip32@npm:1.1.5": version: 1.1.5 resolution: "@scure/bip32@npm:1.1.5" @@ -2295,6 +2325,17 @@ __metadata: languageName: node linkType: hard +"@scure/bip32@npm:1.3.2": + version: 1.3.2 + resolution: "@scure/bip32@npm:1.3.2" + dependencies: + "@noble/curves": ~1.2.0 + "@noble/hashes": ~1.3.2 + "@scure/base": ~1.1.2 + checksum: c5ae84fae43490853693b481531132b89e056d45c945fc8b92b9d032577f753dfd79c5a7bbcbf0a7f035951006ff0311b6cf7a389e26c9ec6335e42b20c53157 + languageName: node + linkType: hard + "@scure/bip39@npm:1.1.1": version: 1.1.1 resolution: "@scure/bip39@npm:1.1.1" @@ -3080,22 +3121,6 @@ __metadata: languageName: node linkType: hard -"@venusprotocol/isolated-pools@npm:^2.0.0": - version: 2.2.0 - resolution: "@venusprotocol/isolated-pools@npm:2.2.0" - dependencies: - "@nomiclabs/hardhat-ethers": ^2.2.3 - "@openzeppelin/contracts": ^4.8.3 - "@openzeppelin/contracts-upgradeable": ^4.8.3 - "@openzeppelin/hardhat-upgrades": ^1.21.0 - "@solidity-parser/parser": ^0.13.2 - ethers: ^5.7.0 - hardhat-deploy: ^0.11.14 - module-alias: ^2.2.2 - checksum: f956867ebe199724b4abceac518d2591729455c486ef76fe8a6d7f27fab73cb46883c72aff694084229f08d8c2e24e74a7b121e556a7ca75040a37e7b4126361 - languageName: node - linkType: hard - "@venusprotocol/keeper-bots@workspace:.": version: 0.0.0-use.local resolution: "@venusprotocol/keeper-bots@workspace:." @@ -3123,7 +3148,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^6.13.1 "@typescript-eslint/parser": ^6.13.1 "@venusprotocol/oracle": ^1.7.3 - "@venusprotocol/protocol-reserve": 1.0.0-converters.1 + "@venusprotocol/protocol-reserve": 1.3.0-dev.1 "@venusprotocol/solidity-utilities": ^1.1.0 "@venusprotocol/venus-protocol": 6.1.0-dev.8 chai: ^4.3.10 @@ -3149,6 +3174,7 @@ __metadata: ts-node: ^10.9.1 typechain: ^8.3.2 typescript: ^5.3.2 + viem: ^2.1.1 languageName: unknown linkType: soft @@ -3172,27 +3198,26 @@ __metadata: languageName: node linkType: hard -"@venusprotocol/protocol-reserve@npm:1.0.0-converters.1": - version: 1.0.0-converters.1 - resolution: "@venusprotocol/protocol-reserve@npm:1.0.0-converters.1" +"@venusprotocol/protocol-reserve@npm:1.2.0-dev.2": + version: 1.2.0-dev.2 + resolution: "@venusprotocol/protocol-reserve@npm:1.2.0-dev.2" dependencies: "@nomiclabs/hardhat-ethers": ^2.2.3 "@openzeppelin/contracts": ^4.8.3 "@openzeppelin/contracts-upgradeable": ^4.8.3 "@openzeppelin/hardhat-upgrades": ^1.21.0 "@solidity-parser/parser": ^0.13.2 - "@venusprotocol/isolated-pools": ^2.0.0 "@venusprotocol/solidity-utilities": ^1.0.1 ethers: ^5.7.0 hardhat-deploy: ^0.11.14 module-alias: ^2.2.2 - checksum: 6d5ead8b99a2e0cd0b0afd5b610eb83819c948ddd43de75361e1b4489ddfb1647cd63877a9dff6473b957250d85f38e4779876e871e73e8d74a6fd2c0b833b27 + checksum: e644bc58b6b4646ce6b262e9e34646d65285dee97cb38a34ac99d35330b3f4bea4c9075d87ee6e6d1e813563c622a01e12eba82014e6d7952e17ea798673bac3 languageName: node linkType: hard -"@venusprotocol/protocol-reserve@npm:1.2.0-dev.2": - version: 1.2.0-dev.2 - resolution: "@venusprotocol/protocol-reserve@npm:1.2.0-dev.2" +"@venusprotocol/protocol-reserve@npm:1.3.0-dev.1": + version: 1.3.0-dev.1 + resolution: "@venusprotocol/protocol-reserve@npm:1.3.0-dev.1" dependencies: "@nomiclabs/hardhat-ethers": ^2.2.3 "@openzeppelin/contracts": ^4.8.3 @@ -3203,7 +3228,7 @@ __metadata: ethers: ^5.7.0 hardhat-deploy: ^0.11.14 module-alias: ^2.2.2 - checksum: e644bc58b6b4646ce6b262e9e34646d65285dee97cb38a34ac99d35330b3f4bea4c9075d87ee6e6d1e813563c622a01e12eba82014e6d7952e17ea798673bac3 + checksum: 2c64c6ee4c3b1daf684acbbe7ee443c2cce369028f727c70b0d7236055a19da194cdad56929021a4ab4e21ce9ddbc45e08b2f9d2370dbc4730d8c9a695d52008 languageName: node linkType: hard @@ -3345,6 +3370,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:0.10.0": + version: 0.10.0 + resolution: "abitype@npm:0.10.0" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: 01a75393740036121414024aa7ed61e6a2104bfd90c91b6aa1a7778cf1edfa15b828779acbbb13ac641939d1ba9c836d143d9f7310699cd7496273bb24c599b3 + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -7692,6 +7732,15 @@ __metadata: languageName: node linkType: hard +"isows@npm:1.0.3": + version: 1.0.3 + resolution: "isows@npm:1.0.3" + peerDependencies: + ws: "*" + checksum: 9cacd5cf59f67deb51e825580cd445ab1725ecb05a67c704050383fb772856f3cd5e7da8ad08f5a3bd2823680d77d099459d0c6a7037972a74d6429af61af440 + languageName: node + linkType: hard + "issue-parser@npm:^6.0.0": version: 6.0.0 resolution: "issue-parser@npm:6.0.0" @@ -12178,6 +12227,27 @@ __metadata: languageName: node linkType: hard +"viem@npm:^2.1.1": + version: 2.1.1 + resolution: "viem@npm:2.1.1" + dependencies: + "@adraffy/ens-normalize": 1.10.0 + "@noble/curves": 1.2.0 + "@noble/hashes": 1.3.2 + "@scure/bip32": 1.3.2 + "@scure/bip39": 1.2.1 + abitype: 0.10.0 + isows: 1.0.3 + ws: 8.13.0 + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 3ff1c402188fd937341476fe46d5849aec5254070236d88625fcfe92d3ef2947e8ab54e3ffed2ba794c2371b6370410fe7663e49ff93d480c575129394ac5ffc + languageName: node + linkType: hard + "walk-up-path@npm:^3.0.1": version: 3.0.1 resolution: "walk-up-path@npm:3.0.1" @@ -12380,6 +12450,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.13.0": + version: 8.13.0 + resolution: "ws@npm:8.13.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c + languageName: node + linkType: hard + "ws@npm:^7.4.6": version: 7.5.9 resolution: "ws@npm:7.5.9"