diff --git a/package.json b/package.json index 4fa9d25..fb5e114 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "set.js", - "version": "0.5.1", + "version": "0.5.2", "description": "A javascript library for interacting with the Set Protocol v2", "keywords": [ "set.js", @@ -58,7 +58,8 @@ "@0xproject/types": "^1.1.4", "@0xproject/typescript-typings": "^3.0.2", "@0xproject/utils": "^2.0.2", - "@setprotocol/set-protocol-v2": "^0.1.10", + "@setprotocol/set-protocol-v2": "^0.1.15", + "@setprotocol/set-v2-strategies": "^0.0.7", "@types/chai-as-promised": "^7.1.3", "@types/jest": "^26.0.5", "@types/web3": "^1.2.2", diff --git a/src/Set.ts b/src/Set.ts index 8824705..785c6e3 100644 --- a/src/Set.ts +++ b/src/Set.ts @@ -16,7 +16,7 @@ 'use strict'; -import { SetJSConfig } from './types'; +import { SetJSConfig, DelegatedManagerSystemExtensions } from './types'; import Assertions from './assertions'; import { BlockchainAPI, @@ -34,6 +34,10 @@ import { PerpV2LeverageAPI, PerpV2LeverageViewerAPI, UtilsAPI, + DelegatedManagerFactoryAPI, + IssuanceExtensionAPI, + TradeExtensionAPI, + StreamingFeeExtensionAPI, } from './api/index'; const ethersProviders = require('ethers').providers; @@ -142,10 +146,15 @@ class Set { public perpV2BasisTradingViewer: PerpV2LeverageViewerAPI; /** - * An instance of the BlockchainAPI class. Contains interfaces for - * interacting with the blockchain + * An instance of DelegatedManagerFactory class. Contains methods for deploying and initializing + * DelegatedManagerSystem deployed SetTokens and Manager contracts */ - public blockchain: BlockchainAPI; + public delegatedManagerFactory: DelegatedManagerFactoryAPI; + + /** + * Group of extension for interacting with SetTokens managed by with DelegatedManager system contracts + */ + public extensions: DelegatedManagerSystemExtensions; /** * An instance of the UtilsAPI class. Contains interfaces for fetching swap quotes from 0x Protocol, @@ -153,6 +162,12 @@ class Set { */ public utils: UtilsAPI; + /** + * An instance of the BlockchainAPI class. Contains interfaces for + * interacting with the blockchain + */ + public blockchain: BlockchainAPI; + /** * Instantiates a new Set instance that provides the public interface to the Set.js library */ @@ -187,6 +202,17 @@ class Set { config.perpV2BasisTradingModuleViewerAddress); this.blockchain = new BlockchainAPI(ethersProvider, assertions); this.utils = new UtilsAPI(ethersProvider, config.zeroExApiKey, config.zeroExApiUrls); + + this.delegatedManagerFactory = new DelegatedManagerFactoryAPI( + ethersProvider, + config.delegatedManagerFactoryAddress + ); + + this.extensions = { + streamingFeeExtension: new StreamingFeeExtensionAPI(ethersProvider, config.streamingFeeExtensionAddress), + issuanceExtension: new IssuanceExtensionAPI(ethersProvider, config.issuanceExtensionAddress), + tradeExtension: new TradeExtensionAPI(ethersProvider, config.tradeExtensionAddress), + }; } } diff --git a/src/api/DelegatedManagerFactoryAPI.ts b/src/api/DelegatedManagerFactoryAPI.ts new file mode 100644 index 0000000..06b62f5 --- /dev/null +++ b/src/api/DelegatedManagerFactoryAPI.ts @@ -0,0 +1,187 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { ContractTransaction, BigNumberish, BytesLike } from 'ethers'; +import { Provider } from '@ethersproject/providers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; + +import DelegatedManagerFactoryWrapper from '../wrappers/set-v2-strategies/DelegatedManagerFactoryWrapper'; +import Assertions from '../assertions'; + +/** + * @title DelegatedManagerFactoryAPI + * @author Set Protocol + * + * The DelegatedManagerFactoryAPI exposes methods to create and initialized new SetTokens bundled with + * Manager contracts and extensions which encode fee management and rebalance trading logic. The API also + * provides some helper methods to generate bytecode data packets that encode module and extension + * initialization method calls. + * + */ +export default class DelegatedManagerFactoryAPI { + private DelegatedManagerFactoryWrapper: DelegatedManagerFactoryWrapper; + private assert: Assertions; + + public constructor( + provider: Provider, + delegatedManagerFactoryAddress: Address, + assertions?: Assertions) { + this.DelegatedManagerFactoryWrapper = new DelegatedManagerFactoryWrapper(provider, delegatedManagerFactoryAddress); + this.assert = assertions || new Assertions(); + } + + /** + * Deploys a new SetToken and DelegatedManager. Sets some temporary metadata about the deployment + * which will be consumed during a subsequent intialization step (see `initialize` method) which + * wires everything together. + * + * To interact with the DelegateManager system after this transaction is executed it's necessary + * to get the address of the created SetToken via a SetTokenCreated event logged . + * + * An ethers.js recipe for programatically retrieving the relevant log can be found in `set-protocol-v2`'s + * protocol utilities here: + * + * https://github.com/SetProtocol/set-protocol-v2/blob/master/utils/common/protocolUtils.ts + * + * @param components Addresses of components for initial Positions + * @param units Units. Each unit is the # of components per 10^18 of a SetToken + * @param name Name of the SetToken + * @param symbol Symbol of the SetToken + * @param owner Address to set as the DelegateManager's `owner` role + * @param methodologist Address to set as the DelegateManager's methodologist role + * @param modules Modules to enable. All modules must be approved by the Controller + * @param operators Operators authorized for the DelegateManager + * @param assets Assets DelegateManager can trade. When empty, asset allow list is not enforced + * @param extensions Extensions authorized for the DelegateManager + * @param callerAddress Address of caller (optional) + * @param txOpts Overrides for transaction (optional) + * + * @return Transaction hash of the initialize transaction + */ + public async createSetAndManagerAsync( + components: Address[], + units: BigNumberish[], + name: string, + symbol: string, + owner: Address, + methodologist: Address, + modules: Address[], + operators: Address[], + assets: Address[], + extensions: Address[], + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + this.assert.schema.isValidAddressList('components', components); + this.assert.schema.isValidNumberList('units', units); + this.assert.schema.isValidString('name', name); + this.assert.schema.isValidString('symbol', symbol); + this.assert.schema.isValidAddress('methodologist', methodologist); + this.assert.schema.isValidAddressList('modules', modules); + this.assert.schema.isValidAddressList('operators', operators); + this.assert.schema.isValidAddressList('assets', assets); + this.assert.schema.isValidAddressList('extensions', extensions); + + this.assert.common.isNotEmptyArray( + components, + 'Component addresses must contain at least one component.' + ); + this.assert.common.isEqualLength( + components, + units, + 'Component addresses and units must be equal length.' + ); + + return await this.DelegatedManagerFactoryWrapper.createSetAndManager( + components, + units, + name, + symbol, + owner, + methodologist, + modules, + operators, + assets, + extensions, + callerAddress, + txOpts + ); + } + + /** + * Wires SetToken, DelegatedManager, global manager extensions, and modules together into a functioning + * package. `initializeTargets` and `initializeBytecode` params are isomorphic, e.g the arrays must + * be the same length and the bytecode at `initializeBytecode[i]` will be called on `initializeTargets[i]`; + * + * Use the generateBytecode methods provided by this API to prepare parameters for calls to `initialize` + * as below: + * + * ``` + * tradeModuleBytecodeData = api.getTradeModuleInitializationBytecode(setTokenAddress) + * initializeTargets.push(tradeModuleAddress); + * initializeBytecode.push(tradeModuleBytecodeData); + * ``` + * + * @param setTokenAddress Address of deployed SetToken to initialize + * @param ownerFeeSplit % of fees in precise units (10^16 = 1%) sent to owner, rest to methodologist + * @param ownerFeeRecipient Address which receives operator's share of fees when they're distributed + * @param initializeTargets Addresses of extensions or modules which should be initialized + * @param initializeBytecode Array of encoded calls to a target's initialize function + * @param callerAddress Address of caller (optional) + * @param txOpts Overrides for transaction (optional) + * + * @return Transaction hash of the initialize transaction + */ + public async initializeAsync( + setTokenAddress: Address, + ownerFeeSplit: BigNumberish, + ownerFeeRecipient: Address, + initializeTargets: Address[], + initializeBytecode: BytesLike[], + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + this.assert.schema.isValidAddress('setTokenAddress', setTokenAddress); + this.assert.schema.isValidNumber('ownerFeeSplit', ownerFeeSplit); + this.assert.schema.isValidAddress('ownerFeeRecipient', ownerFeeRecipient); + this.assert.schema.isValidAddressList('initializeTargets', initializeTargets); + this.assert.schema.isValidBytesList('initializeBytecode', initializeBytecode); + + this.assert.common.isNotEmptyArray( + initializeTargets, + 'initializationTargets array must contain at least one element.' + ); + + this.assert.common.isEqualLength( + initializeTargets, + initializeBytecode, + 'initializeTargets and initializeBytecode arrays must be equal length.' + ); + + return await this.DelegatedManagerFactoryWrapper.initialize( + setTokenAddress, + ownerFeeSplit, + ownerFeeRecipient, + initializeTargets, + initializeBytecode, + callerAddress, + txOpts + ); + } +} diff --git a/src/api/extensions/IssuanceExtensionAPI.ts b/src/api/extensions/IssuanceExtensionAPI.ts new file mode 100644 index 0000000..ec1c9f7 --- /dev/null +++ b/src/api/extensions/IssuanceExtensionAPI.ts @@ -0,0 +1,125 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { ContractTransaction, BigNumberish, BytesLike, utils as EthersUtils } from 'ethers'; +import { Provider } from '@ethersproject/providers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; +import { IssuanceExtension__factory } from '@setprotocol/set-v2-strategies/dist/typechain/factories/IssuanceExtension__factory'; + +import IssuanceExtensionWrapper from '../../wrappers/set-v2-strategies/IssuanceExtensionWrapper'; +import Assertions from '../../assertions'; + +/** + * @title IssuanceExtensionAPI + * @author Set Protocol + * + * The IssuanceExtensionAPI exposes methods to distribute and configure issuance fees for SetTokens + * using the DelegatedManager system. The API also provides some helper methods to generate bytecode data packets + * that encode module and extension initialization method calls. + */ +export default class IssuanceExtensionAPI { + private issuanceExtensionWrapper: IssuanceExtensionWrapper; + private assert: Assertions; + + public constructor( + provider: Provider, + IssuanceExtensionAddress: Address, + assertions?: Assertions) { + this.issuanceExtensionWrapper = new IssuanceExtensionWrapper(provider, IssuanceExtensionAddress); + this.assert = assertions || new Assertions(); + } + + /** + * Distributes issuance and redemption fees calculates fees for. Calculates fees for owner and methodologist + * and sends to owner fee recipient and methodologist respectively. (Anyone can call this method.) + * + * @param setTokenAddress Address of the deployed SetToken to distribute fees for + * @param callerAddress Address of caller (optional) + * @param txOpts Overrides for transaction (optional) + */ + public async distributeFeesAsync( + setTokenAddress: Address, + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + this.assert.schema.isValidAddress('setTokenAddress', setTokenAddress); + + return await this.issuanceExtensionWrapper.distributeFees( + setTokenAddress, + callerAddress, + txOpts + ); + } + + /** + * Generates IssuanceExtension initialize call bytecode to be passed as an element in the `initializeBytecode` + * array for the DelegatedManagerFactory's `initializeAsync` method. + * + * @param delegatedManagerAddress Instance of DelegatedManager to initialize the StreamingFeeExtension for + * + * @return Initialization bytecode + */ + public getIssuanceExtensionInitializationBytecode( + delegatedManagerAddress: Address + ): BytesLike { + this.assert.schema.isValidAddress('delegatedManagerAddress', delegatedManagerAddress); + + const moduleInterface = new EthersUtils.Interface(IssuanceExtension__factory.abi); + return moduleInterface.encodeFunctionData('initializeExtension', [ delegatedManagerAddress ]); + } + + /** + * Generates `moduleAndExtensionInitialization` bytecode to be passed as an element in the + * `initializeBytecode` array for the `initializeAsync` method. + * + * @param delegatedManagerAddress Instance of deployed delegatedManager to initialize the IssuanceExtension for + * @param maxManagerFee Maximum fee that can be charged on issue and redeem + * @param managerIssueFee Fee to charge on issuance + * @param managerRedeemFee Fee to charge on redemption + * @param feeRecipient Address to send fees to + * @param managerIssuanceHook Address of contract implementing pre-issuance hook function (ex: SupplyCapHook) + * + * @return Initialization bytecode + */ + public getIssuanceModuleAndExtensionInitializationBytecode( + delegatedManagerAddress: Address, + maxManagerFee: BigNumberish, + managerIssueFee: BigNumberish, + managerRedeemFee: BigNumberish, + feeRecipientAddress: Address, + managerIssuanceHookAddress: Address + ): BytesLike { + this.assert.schema.isValidAddress('delegatedManagerAddress', delegatedManagerAddress); + this.assert.schema.isValidNumber('maxManagerFee', maxManagerFee); + this.assert.schema.isValidNumber('managerIssueFee', managerIssueFee); + this.assert.schema.isValidNumber('managerRedeemFee', managerRedeemFee); + this.assert.schema.isValidAddress('feeRecipientAddress', feeRecipientAddress); + this.assert.schema.isValidAddress('managerIssuanceHookAddress', managerIssuanceHookAddress); + + const moduleInterface = new EthersUtils.Interface(IssuanceExtension__factory.abi); + return moduleInterface.encodeFunctionData('initializeModuleAndExtension', [ + delegatedManagerAddress, + maxManagerFee, + managerIssueFee, + managerRedeemFee, + feeRecipientAddress, + managerIssuanceHookAddress, + ]); + } +} diff --git a/src/api/extensions/StreamingFeeExtensionAPI.ts b/src/api/extensions/StreamingFeeExtensionAPI.ts new file mode 100644 index 0000000..34edb20 --- /dev/null +++ b/src/api/extensions/StreamingFeeExtensionAPI.ts @@ -0,0 +1,124 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { ContractTransaction, BytesLike, utils as EthersUtils } from 'ethers'; +import { Provider } from '@ethersproject/providers'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; +import { Address, StreamingFeeState } from '@setprotocol/set-protocol-v2/utils/types'; +import { StreamingFeeSplitExtension__factory } from '@setprotocol/set-v2-strategies/dist/typechain/factories/StreamingFeeSplitExtension__factory'; + +import StreamingFeeExtensionWrapper from '../../wrappers/set-v2-strategies/StreamingFeeExtensionWrapper'; +import Assertions from '../../assertions'; + +/** + * @title StreamingFeeExtensionAPI + * @author Set Protocol + * + * The StreamingFeeExtensionAPI exposes methods to set issuance and redemption fees for SetTokens using + * the DelegatedManager system. The API also provides some helper methods to generate bytecode data packets + * that encode module and extension initialization method calls. + */ +export default class StreamingFeeExtensionAPI { + private streamingFeeExtensionWrapper: StreamingFeeExtensionWrapper; + private assert: Assertions; + + public constructor( + provider: Provider, + streamingFeeExtensionAddress: Address, + assertions?: Assertions) { + this.streamingFeeExtensionWrapper = new StreamingFeeExtensionWrapper(provider, streamingFeeExtensionAddress); + this.assert = assertions || new Assertions(); + } + + /** + * Accrues fees from streaming fee module. Gets resulting balance after fee accrual, calculates fees for + * owner and methodologist, and sends to owner fee recipient and methodologist respectively. (Anyone can call + * this method.) + * + * @param setTokenAddress Instance of deployed SetToken to accrue & distribute streaming fees for + * @param callerAddress Address of caller (optional) + * @param txOpts Overrides for transaction (optional) + * + * @return ContractTransaction + */ + public async accrueFeesAndDistributeAsync( + setTokenAddress: Address, + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + this.assert.schema.isValidAddress('setTokenAddress', setTokenAddress); + + return await this.streamingFeeExtensionWrapper.accrueFeesAndDistribute( + setTokenAddress, + callerAddress, + txOpts + ); + } + + /** + * Generates StreamingFeeExtension initialize call bytecode to be passed as an element in the `initializeBytecode` + * array for the DelegatedManagerFactory's `initializeAsync` method. + * + * @param delegatedManagerAddress Instance of DelegatedManager to initialize the StreamingFeeExtension for + * + * @return Initialization bytecode + */ + public getStreamingFeeExtensionInitializationBytecode( + delegatedManagerAddress: Address + ): BytesLike { + this.assert.schema.isValidAddress('delegatedManagerAddress', delegatedManagerAddress); + + const extensionInterface = new EthersUtils.Interface(StreamingFeeSplitExtension__factory.abi); + return extensionInterface.encodeFunctionData('initializeExtension', [ delegatedManagerAddress ]); + } + + /** + * Generates `moduleAndExtensionInitialization` bytecode to be passed as an element in the + * `initializeBytecode` array for the `initializeAsync` method. + * + * FeeSettings is an object with the properties: + * ``` + * { + * feeRecipient; Address to accrue fees to. (Should be the DelegatedManager contract) + * maxStreamingFeePercentage; Max streaming fee manager commits to using (1% = 1e16, 100% = 1e18) + * streamingFeePercentage; Percent of Set accruing to manager annually (1% = 1e16, 100% = 1e18) + * lastStreamingFeeTimestamp; Timestamp last streaming fee was accrued (Should be 0 on init) + * } + * ``` + * @param setTokenAddress Address of deployed SetToken to initialize the StreamingFeeModule for + * @param feeSettings % of fees in precise units (10^16 = 1%) sent to owner, rest to methodologist + * + * @return Initialization bytecode + */ + public getStreamingFeeModuleAndExtensionInitializationBytecode( + delegatedManagerAddress: Address, + feeSettings: StreamingFeeState + ): BytesLike { + this.assert.schema.isValidAddress('delegatedManagerAddress', delegatedManagerAddress); + this.assert.schema.isValidAddress('feeSettings.feeRecipient', feeSettings.feeRecipient); + this.assert.schema.isValidNumber('feeSettings.maxStreamingFeePercentage', feeSettings.maxStreamingFeePercentage); + this.assert.schema.isValidNumber('feeSettings.streamingFeePercentage', feeSettings.streamingFeePercentage); + this.assert.schema.isValidNumber('feeSettings.lastStreamingFeeTimestamp', feeSettings.lastStreamingFeeTimestamp); + + const extensionInterface = new EthersUtils.Interface(StreamingFeeSplitExtension__factory.abi); + return extensionInterface.encodeFunctionData('initializeModuleAndExtension', [ + delegatedManagerAddress, + feeSettings, + ]); + } +} diff --git a/src/api/extensions/TradeExtensionAPI.ts b/src/api/extensions/TradeExtensionAPI.ts new file mode 100644 index 0000000..a2129bf --- /dev/null +++ b/src/api/extensions/TradeExtensionAPI.ts @@ -0,0 +1,128 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { ContractTransaction, BigNumberish, BytesLike, utils as EthersUtils } from 'ethers'; +import { Provider } from '@ethersproject/providers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; +import { TradeExtension__factory } from '@setprotocol/set-v2-strategies/dist/typechain/factories/TradeExtension__factory'; + +import TradeExtensionWrapper from '../../wrappers/set-v2-strategies/TradeExtensionWrapper'; +import Assertions from '../../assertions'; + +/** + * @title TradeExtensionAPI + * @author Set Protocol + * + * The TradeExtensionAPI exposes methods to trade SetToken components using the TradeModule for SetTokens using + * the DelegatedManager system. The API also provides some helper methods to generate bytecode data packets + * that encode module and extension initialization method calls. + */ +export default class TradeExtensionAPI { + private tradeExtensionWrapper: TradeExtensionWrapper; + private assert: Assertions; + + public constructor( + provider: Provider, + TradeExtensionAddress: Address, + assertions?: Assertions) { + this.tradeExtensionWrapper = new TradeExtensionWrapper(provider, TradeExtensionAddress); + this.assert = assertions || new Assertions(); + } + + /** + * Executes a trade on a supported DEX. Must be called an address authorized for the `operator` role + * on the TradeExtension + * + * NOTE: Although the SetToken units are passed in for the send and receive quantities, the total quantity + * sent and received is the quantity of SetToken units multiplied by the SetToken totalSupply. + * + * @param setTokenAddress Address of the deployed SetToken to trade on behalf of + * @param exchangeName Human readable name of the exchange in the integrations registry + * @param sendToken Address of the token to be sent to the exchange + * @param sendQuantity Units of token in SetToken sent to the exchange + * @param receiveToken Address of the token that will be received from the exchange + * @param minReceiveQuantity Min units of token in SetToken to be received from the exchange + * @param data Arbitrary bytes to be used to construct trade call data + * @param callerAddress Address of caller (optional) + * @param txOpts Overrides for transaction (optional) + */ + public async tradeWithOperatorAsync( + setTokenAddress: Address, + exchangeName: Address, + sendToken: Address, + sendQuantity: BigNumberish, + receiveToken: Address, + minReceiveQuantity: BigNumberish, + data: BytesLike, + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + this.assert.schema.isValidAddress('setTokenAddress', setTokenAddress); + this.assert.schema.isValidString('exchangeName', exchangeName); + this.assert.schema.isValidAddress('sendToken', sendToken); + this.assert.schema.isValidNumber('sendQuantity', sendQuantity); + this.assert.schema.isValidAddress('receiveToken', receiveToken); + this.assert.schema.isValidNumber('minReceiveQuantity', minReceiveQuantity); + this.assert.schema.isValidBytes('data', data); + + return await this.tradeExtensionWrapper.tradeWithOperator( + setTokenAddress, + exchangeName, + sendToken, + sendQuantity, + receiveToken, + minReceiveQuantity, + data, + callerAddress, + txOpts + ); + } + + /** + * Generates TradeExtension initialize call bytecode to be passed as an element in the `initializeBytecode` + * array for the DelegatedManagerFactory's `initializeAsync` method. + * + * @param delegatedManagerAddress Instance of deployed DelegatedManager to initialize the TradeExtension for + * + * @return Initialization bytecode + */ + public getTradeExtensionInitializationBytecode( + delegatedManagerAddress: Address + ): BytesLike { + this.assert.schema.isValidAddress('delegatedManagerAddress', delegatedManagerAddress); + + const extensionInterface = new EthersUtils.Interface(TradeExtension__factory.abi); + return extensionInterface.encodeFunctionData('initializeExtension', [ delegatedManagerAddress ]); + } + + /** + * Generates `moduleAndExtensionInitialization` bytecode to be passed as an element in the `initializeBytecode` + * array for the `initializeAsync` method. + * + * @param setTokenAddress Instance of deployed setToken to initialize the TradeModule for + * + * @return Initialization bytecode + */ + public getTradeModuleAndExtensionInitializationBytecode(delegatedManagerAddress: Address): BytesLike { + this.assert.schema.isValidAddress('delegatedManagerAddress', delegatedManagerAddress); + + const extensionInterface = new EthersUtils.Interface(TradeExtension__factory.abi); + return extensionInterface.encodeFunctionData('initializeModuleAndExtension', [ delegatedManagerAddress ]); + } +} diff --git a/src/api/index.ts b/src/api/index.ts index 72c7836..4d5cd52 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -13,6 +13,11 @@ import SlippageIssuanceAPI from './SlippageIssuanceAPI'; import PerpV2LeverageAPI from './PerpV2LeverageAPI'; import PerpV2LeverageViewerAPI from './PerpV2LeverageViewerAPI'; import UtilsAPI from './UtilsAPI'; +import DelegatedManagerFactoryAPI from './DelegatedManagerFactoryAPI'; +import IssuanceExtensionAPI from './extensions/IssuanceExtensionAPI'; +import StreamingFeeExtensionAPI from './extensions/StreamingFeeExtensionAPI'; +import TradeExtensionAPI from './extensions/TradeExtensionAPI'; + import { TradeQuoter, CoinGeckoDataService, @@ -35,6 +40,10 @@ export { PerpV2LeverageAPI, PerpV2LeverageViewerAPI, UtilsAPI, + DelegatedManagerFactoryAPI, + IssuanceExtensionAPI, + StreamingFeeExtensionAPI, + TradeExtensionAPI, TradeQuoter, CoinGeckoDataService, GasOracleService diff --git a/src/assertions/SchemaAssertions.ts b/src/assertions/SchemaAssertions.ts index 904acb7..8b46254 100644 --- a/src/assertions/SchemaAssertions.ts +++ b/src/assertions/SchemaAssertions.ts @@ -71,6 +71,18 @@ export class SchemaAssertions { this.assertConformsToSchema(variableName, value, schemas.bytesSchema); } + /** + * Throws if a given input is not a valid list of Byte Strings. + * + * @param variableName Variable name being validated. Used for displaying error messages. + * @param values Values being validated. + */ + public isValidBytesList(variableName: string, values: any) { + for (const value of values) { + this.assertConformsToSchema(variableName, value, schemas.bytesSchema); + } + } + /** * Throws if a given input is not a number. * @@ -81,6 +93,18 @@ export class SchemaAssertions { this.assertConformsToSchema(variableName, value, schemas.numberSchema); } + /** + * Throws if a given input is not a valid list of numbers. + * + * @param variableName Variable name being validated. Used for displaying error messages. + * @param values Values being validated. + */ + public isValidNumberList(variableName: string, values: any) { + for (const value of values) { + this.assertConformsToSchema(variableName, value, schemas.numberSchema); + } + } + /** * Throws if a given input is not a whole number. * diff --git a/src/types/common.ts b/src/types/common.ts index 9f9bf2e..173a84d 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -4,6 +4,12 @@ import { Address } from '@setprotocol/set-protocol-v2/utils/types'; import { BigNumber } from 'ethers/lib/ethers'; import { ZeroExApiUrls } from './utils'; +import type { + IssuanceExtensionAPI, + TradeExtensionAPI, + StreamingFeeExtensionAPI +} from '../api'; + export { TransactionReceipt } from 'ethereum-types'; /** @@ -33,6 +39,10 @@ export interface SetJSConfig { perpV2LeverageModuleViewerAddress: Address; perpV2BasisTradingModuleAddress: Address; perpV2BasisTradingModuleViewerAddress: Address; + delegatedManagerFactoryAddress: Address; + issuanceExtensionAddress: Address; + tradeExtensionAddress: Address; + streamingFeeExtensionAddress: Address; } export type SetDetails = { @@ -150,3 +160,9 @@ export type VAssetDisplayInfo = { indexPrice: BigNumber; // 10^18 decimals currentLeverageRatio: BigNumber; // 10^18 decimals }; + +export type DelegatedManagerSystemExtensions = { + issuanceExtension: IssuanceExtensionAPI, + tradeExtension: TradeExtensionAPI, + streamingFeeExtension: StreamingFeeExtensionAPI +}; diff --git a/src/wrappers/set-protocol-v2/ContractWrapper.ts b/src/wrappers/set-protocol-v2/ContractWrapper.ts index e517e0a..a856085 100644 --- a/src/wrappers/set-protocol-v2/ContractWrapper.ts +++ b/src/wrappers/set-protocol-v2/ContractWrapper.ts @@ -36,6 +36,7 @@ import { PerpV2LeverageModuleViewer, PriceOracle, } from '@setprotocol/set-protocol-v2/typechain'; + import { BasicIssuanceModule__factory } from '@setprotocol/set-protocol-v2/dist/typechain/factories/BasicIssuanceModule__factory'; import { DebtIssuanceModule__factory } from '@setprotocol/set-protocol-v2/dist/typechain/factories/DebtIssuanceModule__factory'; import { DebtIssuanceModuleV2__factory } from '@setprotocol/set-protocol-v2/dist/typechain/factories/DebtIssuanceModuleV2__factory'; diff --git a/src/wrappers/set-v2-strategies/ContractWrapper.ts b/src/wrappers/set-v2-strategies/ContractWrapper.ts new file mode 100644 index 0000000..c4c0f40 --- /dev/null +++ b/src/wrappers/set-v2-strategies/ContractWrapper.ts @@ -0,0 +1,167 @@ +/* + Copyright 2020 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { Provider, JsonRpcProvider } from '@ethersproject/providers'; +import { Contract } from 'ethers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; + +import { + DelegatedManagerFactory, + StreamingFeeSplitExtension, + TradeExtension, + IssuanceExtension +} from '@setprotocol/set-v2-strategies/typechain'; + +import { + DelegatedManagerFactory__factory +} from '@setprotocol/set-v2-strategies/dist/typechain/factories/DelegatedManagerFactory__factory'; +import { + StreamingFeeSplitExtension__factory +} from '@setprotocol/set-v2-strategies/dist/typechain/factories/StreamingFeeSplitExtension__factory'; +import { + TradeExtension__factory, +} from '@setprotocol/set-v2-strategies/dist/typechain/factories/TradeExtension__factory'; +import { + IssuanceExtension__factory +} from '@setprotocol/set-v2-strategies/dist/typechain/factories/IssuanceExtension__factory'; + + +/** + * @title ContractWrapper + * @author Set Protocol + * + * The Contracts API handles all functions that load contracts for set-v2-strategies + * + */ +export default class ContractWrapper { + private provider: Provider; + private cache: { [contractName: string]: Contract }; + + public constructor(provider: Provider) { + this.provider = provider; + this.cache = {}; + } + + /** + * Load DelegatedManagerFactory contract + * + * @param DelegatedManagerFactoryAddress Address of the DelegatedManagerFactory instance + * @param callerAddress Address of caller, uses first one on node if none provided. + * @return DelegatedManagerFactory contract instance + */ + public async loadDelegatedManagerFactoryAsync( + delegatedManagerFactoryAddress: Address, + callerAddress?: Address, + ): Promise { + const signer = (this.provider as JsonRpcProvider).getSigner(callerAddress); + const cacheKey = `DelegatedManagerFactory_${delegatedManagerFactoryAddress}_${await signer.getAddress()}`; + + if (cacheKey in this.cache) { + return this.cache[cacheKey] as DelegatedManagerFactory; + } else { + const delegatedManagerFactoryContract = DelegatedManagerFactory__factory.connect( + delegatedManagerFactoryAddress, + signer + ); + + this.cache[cacheKey] = delegatedManagerFactoryContract; + return delegatedManagerFactoryContract; + } + } + + /** + * Load StreamingFeeSplitExtension contract + * + * @param streamingFeeSplitExtension Address of the StreamingFeeSplitExtension instance + * @param callerAddress Address of caller, uses first one on node if none provided. + * @return StreamingFeeSplitExtension contract instance + */ + public async loadStreamingFeeExtensionAsync( + streamingFeeSplitExtensionAddress: Address, + callerAddress?: Address, + ): Promise { + const signer = (this.provider as JsonRpcProvider).getSigner(callerAddress); + const cacheKey = `StreamingFeeSplitExtension_${streamingFeeSplitExtensionAddress}_${await signer.getAddress()}`; + + if (cacheKey in this.cache) { + return this.cache[cacheKey] as StreamingFeeSplitExtension; + } else { + const streamingFeeSplitExtensionContract = StreamingFeeSplitExtension__factory.connect( + streamingFeeSplitExtensionAddress, + signer + ); + + this.cache[cacheKey] = streamingFeeSplitExtensionContract; + return streamingFeeSplitExtensionContract; + } + } + + /** + * Load TradeExtension contract + * + * @param TradeExtension Address of the TradeExtension instance + * @param callerAddress Address of caller, uses first one on node if none provided. + * @return TradeExtension contract instance + */ + public async loadTradeExtensionAsync( + tradeExtensionAddress: Address, + callerAddress?: Address, + ): Promise { + const signer = (this.provider as JsonRpcProvider).getSigner(callerAddress); + const cacheKey = `tradeExtension_${tradeExtensionAddress}_${await signer.getAddress()}`; + + if (cacheKey in this.cache) { + return this.cache[cacheKey] as TradeExtension; + } else { + const tradeExtensionContract = TradeExtension__factory.connect( + tradeExtensionAddress, + signer + ); + + this.cache[cacheKey] = tradeExtensionContract; + return tradeExtensionContract; + } + } + + /** + * Load IssuanceExtension contract + * + * @param IssuanceExtension Address of the IssuanceExtension instance + * @param callerAddress Address of caller, uses first one on node if none provided. + * @return TradeExtension contract instance + */ + public async loadIssuanceExtensionAsync( + issuanceExtensionAddress: Address, + callerAddress?: Address, + ): Promise { + const signer = (this.provider as JsonRpcProvider).getSigner(callerAddress); + const cacheKey = `issuanceExtension_${issuanceExtensionAddress}_${await signer.getAddress()}`; + + if (cacheKey in this.cache) { + return this.cache[cacheKey] as IssuanceExtension; + } else { + const issuanceExtensionContract = IssuanceExtension__factory.connect( + issuanceExtensionAddress, + signer + ); + + this.cache[cacheKey] = issuanceExtensionContract; + return issuanceExtensionContract; + } + } +} diff --git a/src/wrappers/set-v2-strategies/DelegatedManagerFactoryWrapper.ts b/src/wrappers/set-v2-strategies/DelegatedManagerFactoryWrapper.ts new file mode 100644 index 0000000..ffce56d --- /dev/null +++ b/src/wrappers/set-v2-strategies/DelegatedManagerFactoryWrapper.ts @@ -0,0 +1,150 @@ +/* + Copyright 2022 Set Labs Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { BigNumberish, BytesLike, ContractTransaction } from 'ethers'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; +import { Provider } from '@ethersproject/providers'; +import { generateTxOpts } from '../../utils/transactions'; + +import ContractWrapper from './ContractWrapper'; + +/** + * @title DelegatedManagerFactoryWrapper + * @author Set Protocol + * + * The DelegatedManagerFactoryWrapper forwards functionality from the DelegatedManagerFactory contract. + * + */ +export default class DelegatedManagerFactoryWrapper { + private provider: Provider; + private contracts: ContractWrapper; + + private delegatedManagerFactoryAddress: Address; + + public constructor(provider: Provider, delegatedManagerFactoryAddress: Address) { + this.provider = provider; + this.contracts = new ContractWrapper(this.provider); + this.delegatedManagerFactoryAddress = delegatedManagerFactoryAddress; + } + + /** + * Deploys a new SetToken and DelegatedManager. Sets some temporary metadata about the deployment + * which will be consumed during a subsequent intialization step (see `initialize` method) which + * wires everything together. + * + * To interact with the DelegateManager system after this transaction is executed it's necessary + * to get the address of the created SetToken via a SetTokenCreated event logged . + * + * An ethers.js recipe for programatically retrieving the relevant log can be found in `set-protocol-v2`'s + * protocol utilities here: + * + * https://github.com/SetProtocol/set-protocol-v2/blob/master/utils/common/protocolUtils.ts + * + * @param components Addresses of components for initial Positions + * @param units Units. Each unit is the # of components per 10^18 of a SetToken + * @param name Name of the SetToken + * @param symbol Symbol of the SetToken + * @param owner Address to set as the DelegateManager's `owner` role + * @param methodologist Address to set as the DelegateManager's methodologist role + * @param modules Modules to enable. All modules must be approved by the Controller + * @param operators Operators authorized for the DelegateManager + * @param assets Assets DelegateManager can trade. When empty, asset allow list is not enforced + * @param extensions Extensions authorized for the DelegateManager + * @param callerAddress Address of caller (optional) + * @param txOpts Overrides for transaction (optional) + */ + public async createSetAndManager( + components: Address[], + units: BigNumberish[], + name: string, + symbol: string, + owner: Address, + methodologist: Address, + modules: Address[], + operators: Address[], + assets: Address[], + extensions: Address[], + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + const txOptions = await generateTxOpts(txOpts); + const delegatedManagerFactoryInstance = await this.contracts.loadDelegatedManagerFactoryAsync( + this.delegatedManagerFactoryAddress, + callerAddress + ); + + return await delegatedManagerFactoryInstance.createSetAndManager( + components, + units, + name, + symbol, + owner, + methodologist, + modules, + operators, + assets, + extensions, + txOptions, + ); + } + + /** + * Wires SetToken, DelegatedManager, global manager extensions, and modules together into a functioning + * package. `initializeTargets` and `initializeBytecode` params are isomorphic, e.g the arrays must + * be the same length and the bytecode at `initializeBytecode[i]` will be called on `initializeTargets[i]`; + * + * Use the generateBytecode methods provided by this API to prepare parameters for calls to `initialize` + * as below: + * + * ``` + * tradeModuleBytecodeData = api.getTradeModuleInitializationBytecode(setTokenAddress) + * initializeTargets.push(tradeModuleAddress); + * initializeBytecode.push(tradeModuleBytecodeData); + * ``` + * + * @param setTokenAddress Address of deployed SetToken to initialize + * @param ownerFeeSplit % of fees in precise units (10^16 = 1%) sent to owner, rest to methodologist + * @param ownerFeeRecipient Address which receives operator's share of fees when they're distributed + * @param initializeTargets Addresses of extensions or modules which should be initialized + * @param initializedBytecode Array of encoded calls to a target's initialize function + * @param callerAddress Address of caller (optional) + * @param txOpts Overrides for transaction (optional) + */ + public async initialize( + setTokenAddress: Address, + ownerFeeSplit: BigNumberish, + ownerFeeRecipient: Address, + intializeTargets: Address[], + initializeBytecode: BytesLike[], + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + const txOptions = await generateTxOpts(txOpts); + const delegatedManagerFactoryInstance = await this.contracts.loadDelegatedManagerFactoryAsync( + this.delegatedManagerFactoryAddress, + callerAddress + ); + + return await delegatedManagerFactoryInstance.initialize( + setTokenAddress, + ownerFeeSplit, + ownerFeeRecipient, + intializeTargets, + initializeBytecode, + txOptions, + ); + } +} \ No newline at end of file diff --git a/src/wrappers/set-v2-strategies/IssuanceExtensionWrapper.ts b/src/wrappers/set-v2-strategies/IssuanceExtensionWrapper.ts new file mode 100644 index 0000000..6f156ef --- /dev/null +++ b/src/wrappers/set-v2-strategies/IssuanceExtensionWrapper.ts @@ -0,0 +1,64 @@ +/* + Copyright 2022 Set Labs Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { ContractTransaction } from 'ethers'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; +import { Provider } from '@ethersproject/providers'; +import { generateTxOpts } from '../../utils/transactions'; + +import ContractWrapper from './ContractWrapper'; + +/** + * @title IssuanceExtensionWrapper + * @author Set Protocol + * + * The IssuanceExtensionWrapper forwards functionality from the IssuanceExtension contract. + * + */ +export default class IssuanceExtensionWrapper { + private provider: Provider; + private contracts: ContractWrapper; + + private issuanceExtensionAddress: Address; + + public constructor(provider: Provider, issuanceExtensionAddress: Address) { + this.provider = provider; + this.contracts = new ContractWrapper(this.provider); + this.issuanceExtensionAddress = issuanceExtensionAddress; + } + + /** + * Distributes issuance and redemption fees calculates fees for. Calculates fees for owner and methodologist + * and sends to owner fee recipient and methodologist respectively. (Anyone can call this method.) + * + * @param setTokenAddress Instance of deployed SetToken to distribute issuance fees for + * + * @return Initialization bytecode + */ + public async distributeFees( + setTokenAddress: Address, + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + const txOptions = await generateTxOpts(txOpts); + const issuanceExtensionInstance = await this.contracts.loadIssuanceExtensionAsync( + this.issuanceExtensionAddress, + callerAddress + ); + + return await issuanceExtensionInstance.distributeFees(setTokenAddress, txOptions); + } +} \ No newline at end of file diff --git a/src/wrappers/set-v2-strategies/StreamingFeeExtensionWrapper.ts b/src/wrappers/set-v2-strategies/StreamingFeeExtensionWrapper.ts new file mode 100644 index 0000000..d3155c1 --- /dev/null +++ b/src/wrappers/set-v2-strategies/StreamingFeeExtensionWrapper.ts @@ -0,0 +1,65 @@ +/* + Copyright 2022 Set Labs Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { ContractTransaction } from 'ethers'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; +import { Provider } from '@ethersproject/providers'; +import { generateTxOpts } from '../../utils/transactions'; + +import ContractWrapper from './ContractWrapper'; + +/** + * @title StreamingFeeExtensionWrapper + * @author Set Protocol + * + * The StreamingFeeExtensionWrapper forwards functionality from the StreamingFeeExtension contract. + * + */ +export default class StreamingFeeExtensionWrapper { + private provider: Provider; + private contracts: ContractWrapper; + + private streamingFeeExtensionAddress: Address; + + public constructor(provider: Provider, streamingFeeExtensionAddress: Address) { + this.provider = provider; + this.contracts = new ContractWrapper(this.provider); + this.streamingFeeExtensionAddress = streamingFeeExtensionAddress; + } + + /** + * Accrues fees from streaming fee module. Gets resulting balance after fee accrual, calculates fees for + * owner and methodologist, and sends to owner fee recipient and methodologist respectively. (Anyone can call + * this method.) + * + * @param setTokenAddress Instance of deployed SetToken to accrue & distribute streaming fees for + * + * @return Initialization bytecode + */ + public async accrueFeesAndDistribute( + setTokenAddress: Address, + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + const txOptions = await generateTxOpts(txOpts); + const streamingFeeExtensionInstance = await this.contracts.loadStreamingFeeExtensionAsync( + this.streamingFeeExtensionAddress, + callerAddress + ); + + return await streamingFeeExtensionInstance.accrueFeesAndDistribute(setTokenAddress, txOptions); + } +} \ No newline at end of file diff --git a/src/wrappers/set-v2-strategies/TradeExtensionWrapper.ts b/src/wrappers/set-v2-strategies/TradeExtensionWrapper.ts new file mode 100644 index 0000000..4a9ad40 --- /dev/null +++ b/src/wrappers/set-v2-strategies/TradeExtensionWrapper.ts @@ -0,0 +1,88 @@ +/* + Copyright 2022 Set Labs Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { BigNumberish, BytesLike, ContractTransaction } from 'ethers'; +import { TransactionOverrides } from '@setprotocol/set-protocol-v2/dist/typechain'; +import { Provider } from '@ethersproject/providers'; +import { generateTxOpts } from '../../utils/transactions'; + +import ContractWrapper from './ContractWrapper'; + +/** + * @title TradeExtensionWrapper + * @author Set Protocol + * + * The TradeExtensionWrapper forwards functionality from the TradeExtension contract. + * + */ +export default class TradeExtensionWrapper { + private provider: Provider; + private contracts: ContractWrapper; + + private tradeExtensionAddress: Address; + + public constructor(provider: Provider, tradeExtensionAddress: Address) { + this.provider = provider; + this.contracts = new ContractWrapper(this.provider); + this.tradeExtensionAddress = tradeExtensionAddress; + } + + /** + * Executes a trade on a supported DEX. Must be called an address authorized for the `operator` role + * on the TradeExtension + * + * NOTE: Although the SetToken units are passed in for the send and receive quantities, the total quantity + * sent and received is the quantity of SetToken units multiplied by the SetToken totalSupply. + * + * @param setTokenAddress Address of the deployed SetToken to trade on behalf of + * @param exchangeName Human readable name of the exchange in the integrations registry + * @param sendToken Address of the token to be sent to the exchange + * @param sendQuantity Units of token in SetToken sent to the exchange + * @param receiveToken Address of the token that will be received from the exchange + * @param minReceiveQuantity Min units of token in SetToken to be received from the exchange + * @param data Arbitrary bytes to be used to construct trade call data + * @param callerAddress Address of caller (optional) + * @param txOptions Overrides for transaction (optional) + */ + public async tradeWithOperator( + setTokenAddress: Address, + exchangeName: Address, + sendToken: Address, + sendQuantity: BigNumberish, + receiveToken: Address, + minReceiveQuantity: BigNumberish, + data: BytesLike, + callerAddress: Address = undefined, + txOpts: TransactionOverrides = {} + ): Promise { + const txOptions = await generateTxOpts(txOpts); + const tradeExtensionInstance = await this.contracts.loadTradeExtensionAsync( + this.tradeExtensionAddress, + callerAddress + ); + + return await tradeExtensionInstance.trade( + setTokenAddress, + exchangeName, + sendToken, + sendQuantity, + receiveToken, + minReceiveQuantity, + data, + txOptions + ); + } +} diff --git a/test/api/DelegatedManagerFactoryAPI.spec.ts b/test/api/DelegatedManagerFactoryAPI.spec.ts new file mode 100644 index 0000000..b85cd74 --- /dev/null +++ b/test/api/DelegatedManagerFactoryAPI.spec.ts @@ -0,0 +1,326 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ethers } from 'ethers'; +import { BigNumber, ContractTransaction } from 'ethers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { ether } from '@setprotocol/set-protocol-v2/dist/utils/common'; + +import DelegateManagerFactoryAPI from '@src/api/DelegatedManagerFactoryAPI'; +import DelegatedManagerFactoryWrapper from '@src/wrappers/set-v2-strategies/DelegatedManagerFactoryWrapper'; +import { expect } from '../utils/chai'; + +const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); + +jest.mock('@src/wrappers/set-v2-strategies/DelegatedManagerFactoryWrapper'); + +describe('DelegateManagerFactoryAPI', () => { + let owner: Address; + let methodologist: Address; + let operator: Address; + let componentOne: Address; + let componentTwo: Address; + let module: Address; + let extension: Address; + let delegatedManagerFactory: Address; + let setToken: Address; + let ownerFeeRecipient: Address; + + let delegatedManagerFactoryAPI: DelegateManagerFactoryAPI; + let delegatedManagerFactoryWrapper: DelegatedManagerFactoryWrapper; + + beforeEach(async () => { + [ + owner, + methodologist, + operator, + componentOne, + componentTwo, + module, + extension, + delegatedManagerFactory, + setToken, + ownerFeeRecipient, + ] = await provider.listAccounts(); + + delegatedManagerFactoryAPI = new DelegateManagerFactoryAPI(provider, delegatedManagerFactory); + delegatedManagerFactoryWrapper = (DelegatedManagerFactoryWrapper as any).mock.instances[0]; + }); + + afterEach(() => { + (DelegatedManagerFactoryWrapper as any).mockClear(); + }); + + describe('#createSetAndManagerAsync', () => { + let subjectComponents: Address[]; + let subjectUnits: BigNumber[]; + let subjectName: string; + let subjectSymbol: string; + let subjectOwner: Address; + let subjectMethodologist: Address; + let subjectModules: Address[]; + let subjectOperators: Address[]; + let subjectAssets: Address[]; + let subjectExtensions: Address[]; + let subjectCallerAddress: Address; + let subjectTransactionOptions: any; + + beforeEach(async () => { + subjectComponents = [componentOne, componentTwo]; + subjectUnits = [ether(1), ether(.5)]; + subjectName = 'Test'; + subjectSymbol = 'TEST'; + subjectOwner = owner; + subjectMethodologist = methodologist; + subjectModules = [module]; + subjectOperators = [operator]; + subjectAssets = [componentOne, componentTwo]; + subjectExtensions = [extension]; + subjectCallerAddress = owner; + subjectTransactionOptions = {}; + }); + + async function subject(): Promise { + return delegatedManagerFactoryAPI.createSetAndManagerAsync( + subjectComponents, + subjectUnits, + subjectName, + subjectSymbol, + subjectOwner, + subjectMethodologist, + subjectModules, + subjectOperators, + subjectAssets, + subjectExtensions, + subjectCallerAddress, + subjectTransactionOptions + ); + } + + it('should call `createSetAndManagerAsync` on the DelegatedManagerFactoryWrapper', async () => { + await subject(); + + expect(delegatedManagerFactoryWrapper.createSetAndManager).to.have.beenCalledWith( + subjectComponents, + subjectUnits, + subjectName, + subjectSymbol, + subjectOwner, + subjectMethodologist, + subjectModules, + subjectOperators, + subjectAssets, + subjectExtensions, + subjectCallerAddress, + subjectTransactionOptions + ); + }); + + describe('when a component is not a valid address', () => { + beforeEach(() => subjectComponents = ['0xinvalid', componentTwo]); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when a unit is not a valid number', () => { + beforeEach(() => subjectUnits = [NaN, ether(.5)]); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when a name is not a valid string', () => { + beforeEach(() => subjectName = 5 as string); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when a symbol is not a valid string', () => { + beforeEach(() => subjectSymbol = 5 as string); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when a methodologist is not a valid address', () => { + beforeEach(() => subjectMethodologist = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when a module is not a valid address', () => { + beforeEach(() => subjectModules = ['0xinvalid']); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when an operator is not a valid address', () => { + beforeEach(() => subjectOperators = ['0xinvalid']); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when an asset is not a valid address', () => { + beforeEach(() => subjectAssets = ['0xinvalid']); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when an extension is not a valid address', () => { + beforeEach(() => subjectExtensions = ['0xinvalid']); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when components is an empty array', () => { + beforeEach(() => subjectComponents = []); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Component addresses must contain at least one component.'); + }); + }); + + describe('when components and units have different array lengths', () => { + beforeEach(() => subjectComponents = [componentOne] ); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Component addresses and units must be equal length.'); + }); + }); + }); + + describe('#initializeAsync', () => { + let subjectSetToken: Address; + let subjectOwnerFeeSplit: BigNumber; + let subjectOwnerFeeRecipient: Address; + let subjectInitializeTargets: Address[]; + let subjectInitializeBytecode: string[]; + let subjectCallerAddress: Address; + let subjectTransactionOptions: any; + + beforeEach(async () => { + subjectSetToken = setToken; + subjectOwnerFeeSplit = ether(.5); + subjectOwnerFeeRecipient = ownerFeeRecipient; + subjectInitializeTargets = [module]; + subjectInitializeBytecode = ['0x0123456789ABCDEF']; + subjectCallerAddress = owner; + subjectTransactionOptions = {}; + }); + + async function subject(): Promise { + return delegatedManagerFactoryAPI.initializeAsync( + subjectSetToken, + subjectOwnerFeeSplit, + subjectOwnerFeeRecipient, + subjectInitializeTargets, + subjectInitializeBytecode, + subjectCallerAddress, + subjectTransactionOptions, + ); + } + + it('should call initialize on the DelegatedManagerFactoryWrapper', async () => { + await subject(); + + expect(delegatedManagerFactoryWrapper.initialize).to.have.beenCalledWith( + subjectSetToken, + subjectOwnerFeeSplit, + subjectOwnerFeeRecipient, + subjectInitializeTargets, + subjectInitializeBytecode, + subjectCallerAddress, + subjectTransactionOptions, + ); + }); + + describe('when setToken is not a valid address', () => { + beforeEach(() => subjectSetToken = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when ownerFeeSplit is not a valid number', () => { + beforeEach(() => subjectOwnerFeeSplit = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when ownerFeeRecipient is not a valid address', () => { + beforeEach(() => subjectOwnerFeeRecipient = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when an initializeTarget is not a valid address', () => { + beforeEach(() => subjectInitializeTargets = ['0xinvalid']); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when an initializeBytecode is not a valid string', () => { + beforeEach(() => subjectInitializeBytecode = [5 as string]); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when initializeTargets array is empty', () => { + beforeEach(() => subjectInitializeTargets = []); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith( + 'initializationTargets array must contain at least one element' + ); + }); + }); + + describe('when initializeTargets and initializeBytecode are not equal length', () => { + beforeEach(() => subjectInitializeBytecode = ['0x00', '0x00']); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith( + 'initializeTargets and initializeBytecode arrays must be equal length' + ); + }); + }); + }); +}); diff --git a/test/api/extensions/IssuanceExtensionAPI.spec.ts b/test/api/extensions/IssuanceExtensionAPI.spec.ts new file mode 100644 index 0000000..106ef3b --- /dev/null +++ b/test/api/extensions/IssuanceExtensionAPI.spec.ts @@ -0,0 +1,211 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ethers, BytesLike } from 'ethers'; +import { BigNumber, ContractTransaction } from 'ethers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { ether } from '@setprotocol/set-protocol-v2/dist/utils/common'; + +import IssuanceExtensionAPI from '@src/api/extensions/IssuanceExtensionAPI'; +import IssuanceExtensionWrapper from '@src/wrappers/set-v2-strategies/IssuanceExtensionWrapper'; +import { expect } from '../../utils/chai'; + +const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); + +jest.mock('@src/wrappers/set-v2-strategies/IssuanceExtensionWrapper'); + +describe('IssuanceExtensionAPI', () => { + let owner: Address; + let setToken: Address; + let issuanceExtension: Address; + let delegatedManager: Address; + let feeRecipient: Address; + let managerIssueHook: Address; + + let issuanceExtensionAPI: IssuanceExtensionAPI; + let issuanceExtensionWrapper: IssuanceExtensionWrapper; + + beforeEach(async () => { + [ + owner, + setToken, + issuanceExtension, + delegatedManager, + feeRecipient, + managerIssueHook, + ] = await provider.listAccounts(); + + issuanceExtensionAPI = new IssuanceExtensionAPI(provider, issuanceExtension); + issuanceExtensionWrapper = (IssuanceExtensionWrapper as any).mock.instances[0]; + }); + + afterEach(() => { + (IssuanceExtensionWrapper as any).mockClear(); + }); + + describe('#distributeFeesAsync', () => { + let subjectSetToken: Address; + let subjectCallerAddress: Address; + let subjectTransactionOptions: any; + + beforeEach(async () => { + subjectSetToken = setToken; + subjectCallerAddress = owner; + subjectTransactionOptions = {}; + }); + + async function subject(): Promise { + return issuanceExtensionAPI.distributeFeesAsync( + subjectSetToken, + subjectCallerAddress, + subjectTransactionOptions + ); + } + + it('should call `distribute` on the IssuanceExtensionWrapper', async () => { + await subject(); + + expect(issuanceExtensionWrapper.distributeFees).to.have.beenCalledWith( + subjectSetToken, + subjectCallerAddress, + subjectTransactionOptions + ); + }); + + describe('when a setToken is not a valid address', () => { + beforeEach(() => subjectSetToken = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); + + describe('#getIssuanceExtensionInitializationBytecode', () => { + let subjectDelegatedManager: Address; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager; + }); + + async function subject(): Promise { + return issuanceExtensionAPI.getIssuanceExtensionInitializationBytecode( + subjectDelegatedManager + ); + } + + it('should generate the expected bytecode', async () => { + const expectedBytecode = '0xde2236bd000000000000000000000000e834ec434daba538cd1b9fe1582052b880bd7e63'; + expect(await subject()).eq(expectedBytecode); + }); + + describe('when delegatedManager is not a valid address', () => { + beforeEach(() => subjectDelegatedManager = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); + + describe('#getIssuanceModuleAndExtensionInitializationBytecode', () => { + let subjectDelegatedManager: Address; + let subjectMaxManagerFee: BigNumber; + let subjectManagerIssueFee: BigNumber; + let subjectManagerRedeemFee: BigNumber; + let subjectFeeRecipient: Address; + let subjectManagerIssuanceHook: Address; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager; + subjectMaxManagerFee = ether(.5); + subjectManagerIssueFee = ether(.05); + subjectManagerRedeemFee = ether(.04); + subjectFeeRecipient = feeRecipient; + subjectManagerIssuanceHook = managerIssueHook; + }); + + async function subject(): Promise { + return issuanceExtensionAPI.getIssuanceModuleAndExtensionInitializationBytecode( + subjectDelegatedManager, + subjectMaxManagerFee, + subjectManagerIssueFee, + subjectManagerRedeemFee, + subjectFeeRecipient, + subjectManagerIssuanceHook + ); + } + + it('should generate the expected bytecode', async () => { + const expectedBytecode = + '0xb738ad91000000000000000000000000e834ec434daba538cd1b9fe1582052b880bd7e63000000000000000' + + '00000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000' + + '0000000000b1a2bc2ec50000000000000000000000000000000000000000000000000000008e1bc9bf0400000' + + '0000000000000000000000078dc5d2d739606d31509c31d654056a45185ecb6000000000000000000000000a8' + + 'dda8d7f5310e4a9e24f8eba77e091ac264f872'; + + expect(await subject()).eq(expectedBytecode); + }); + + describe('when setToken is not a valid address', () => { + beforeEach(() => subjectDelegatedManager = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when maxManagerFee is not a valid number', () => { + beforeEach(() => subjectMaxManagerFee = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when managerIssueFee is not a valid number', () => { + beforeEach(() => subjectManagerIssueFee = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when managerRedeemFee is not a valid number', () => { + beforeEach(() => subjectManagerRedeemFee = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when feeRecipient is not a valid address', () => { + beforeEach(() => subjectFeeRecipient = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when managerIssuanceHook is not a valid address', () => { + beforeEach(() => subjectManagerIssuanceHook = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); +}); diff --git a/test/api/extensions/StreamingFeeExtensionAPI.spec.ts b/test/api/extensions/StreamingFeeExtensionAPI.spec.ts new file mode 100644 index 0000000..c46337c --- /dev/null +++ b/test/api/extensions/StreamingFeeExtensionAPI.spec.ts @@ -0,0 +1,193 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ethers, BytesLike } from 'ethers'; +import { BigNumber, ContractTransaction } from 'ethers'; +import { Address, StreamingFeeState } from '@setprotocol/set-protocol-v2/utils/types'; +import { ether } from '@setprotocol/set-protocol-v2/dist/utils/common'; + +import StreamingFeeExtensionAPI from '@src/api/extensions/StreamingFeeExtensionAPI'; +import StreamingFeeExtensionWrapper from '@src/wrappers/set-v2-strategies/StreamingFeeExtensionWrapper'; +import { expect } from '../../utils/chai'; + +const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); + +jest.mock('@src/wrappers/set-v2-strategies/StreamingFeeExtensionWrapper'); + +describe('StreamingFeeExtensionAPI', () => { + let owner: Address; + let setToken: Address; + let streamingFeeExtension: Address; + let delegatedManager: Address; + let feeRecipient: Address; + + let streamingFeeExtensionAPI: StreamingFeeExtensionAPI; + let streamingFeeExtensionWrapper: StreamingFeeExtensionWrapper; + + beforeEach(async () => { + [ + owner, + setToken, + delegatedManager, + streamingFeeExtension, + feeRecipient, + ] = await provider.listAccounts(); + + streamingFeeExtensionAPI = new StreamingFeeExtensionAPI(provider, streamingFeeExtension); + streamingFeeExtensionWrapper = (StreamingFeeExtensionWrapper as any).mock.instances[0]; + }); + + afterEach(() => { + (StreamingFeeExtensionWrapper as any).mockClear(); + }); + + describe('#accrueFeesAndDistributeAsync', () => { + let subjectSetToken: Address; + let subjectCallerAddress: Address; + let subjectTransactionOptions: any; + + beforeEach(async () => { + subjectSetToken = setToken; + subjectCallerAddress = owner; + subjectTransactionOptions = {}; + }); + + async function subject(): Promise { + return streamingFeeExtensionAPI.accrueFeesAndDistributeAsync( + subjectSetToken, + subjectCallerAddress, + subjectTransactionOptions + ); + } + + it('should call `distribute` on the StreamingFeeExtensionWrapper', async () => { + await subject(); + + expect(streamingFeeExtensionWrapper.accrueFeesAndDistribute).to.have.beenCalledWith( + subjectSetToken, + subjectCallerAddress, + subjectTransactionOptions + ); + }); + + describe('when a setToken is not a valid address', () => { + beforeEach(() => subjectSetToken = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); + + describe('#getStreamingFeeExtensionInitializationBytecode', () => { + let subjectDelegatedManager: Address; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager; + }); + + async function subject(): Promise { + return streamingFeeExtensionAPI.getStreamingFeeExtensionInitializationBytecode( + subjectDelegatedManager + ); + } + + it('should generate the expected bytecode', async () => { + const expectedBytecode = '0xde2236bd000000000000000000000000e36ea790bc9d7ab70c55260c66d52b1eca985f84'; + expect(await subject()).eq(expectedBytecode); + }); + + describe('when delegatedManager is not a valid address', () => { + beforeEach(() => subjectDelegatedManager = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); + + describe('#getStreamingFeeModuleAndExtensionInitializationBytecode', () => { + let subjectDelegatedManager: Address; + let subjectFeeSettings: StreamingFeeState; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager; + subjectFeeSettings = { + feeRecipient, + maxStreamingFeePercentage: ether(.1), + streamingFeePercentage: ether(.01), + lastStreamingFeeTimestamp: ether(0), + }; + }); + + async function subject(): Promise { + return streamingFeeExtensionAPI.getStreamingFeeModuleAndExtensionInitializationBytecode( + subjectDelegatedManager, + subjectFeeSettings + ); + } + + it('should generate the expected bytecode', async () => { + const expectedBytecode = + '0x51146818000000000000000000000000e36ea790bc9d7ab70c55260c66d52b1eca985f84000000000000000' + + '00000000078dc5d2d739606d31509c31d654056a45185ecb60000000000000000000000000000000000000000' + + '00000000016345785d8a0000000000000000000000000000000000000000000000000000002386f26fc100000' + + '000000000000000000000000000000000000000000000000000000000000000'; + + expect(await subject()).eq(expectedBytecode); + }); + + describe('when setToken is not a valid address', () => { + beforeEach(() => subjectDelegatedManager = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when feeSettings.feeRecipient is not a valid address', () => { + beforeEach(() => subjectFeeSettings.feeRecipient = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when feeSettings.maxStreamingFeePercentage is not a valid number', () => { + beforeEach(() => subjectFeeSettings.maxStreamingFeePercentage = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when feeSettings.streamingFeePercentage is not a valid number', () => { + beforeEach(() => subjectFeeSettings.streamingFeePercentage = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when feeSettings.lastStreamingFeeTimestamp is not a valid number', () => { + beforeEach(() => subjectFeeSettings.lastStreamingFeeTimestamp = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); +}); diff --git a/test/api/extensions/TradeExtensionAPI.spec.ts b/test/api/extensions/TradeExtensionAPI.spec.ts new file mode 100644 index 0000000..131d0e4 --- /dev/null +++ b/test/api/extensions/TradeExtensionAPI.spec.ts @@ -0,0 +1,213 @@ +/* + Copyright 2022 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ethers, BytesLike } from 'ethers'; +import { BigNumber, ContractTransaction } from 'ethers'; +import { Address } from '@setprotocol/set-protocol-v2/utils/types'; +import { ether } from '@setprotocol/set-protocol-v2/dist/utils/common'; + +import TradeExtensionAPI from '@src/api/extensions/TradeExtensionAPI'; +import TradeExtensionWrapper from '@src/wrappers/set-v2-strategies/TradeExtensionWrapper'; +import { expect } from '../../utils/chai'; + +const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); + +jest.mock('@src/wrappers/set-v2-strategies/TradeExtensionWrapper'); + +describe('TradeExtensionAPI', () => { + let owner: Address; + let setToken: Address; + let tradeExtension: Address; + let delegatedManager: Address; + let sendToken: Address; + let receiveToken: Address; + + let tradeExtensionAPI: TradeExtensionAPI; + let tradeExtensionWrapper: TradeExtensionWrapper; + + beforeEach(async () => { + [ + owner, + setToken, + delegatedManager, + tradeExtension, + sendToken, + receiveToken, + ] = await provider.listAccounts(); + + tradeExtensionAPI = new TradeExtensionAPI(provider, tradeExtension); + tradeExtensionWrapper = (TradeExtensionWrapper as any).mock.instances[0]; + }); + + afterEach(() => { + (TradeExtensionWrapper as any).mockClear(); + }); + + describe('#tradeWithOperatorAsync', () => { + let subjectSetToken: Address; + let subjectExchangeName: string; + let subjectSendToken: Address; + let subjectSendQuantity: BigNumber; + let subjectReceiveToken: Address; + let subjectMinReceiveQuantity: BigNumber; + let subjectData: string; + let subjectCallerAddress: Address; + let subjectTransactionOptions: any; + + beforeEach(async () => { + subjectSetToken = setToken; + subjectExchangeName = 'UniswapV3'; + subjectSendToken = sendToken; + subjectSendQuantity = ether(1); + subjectReceiveToken = receiveToken; + subjectMinReceiveQuantity = ether(.5); + subjectData = '0x123456789abcdedf'; + subjectCallerAddress = owner; + subjectTransactionOptions = {}; + }); + + async function subject(): Promise { + return tradeExtensionAPI.tradeWithOperatorAsync( + subjectSetToken, + subjectExchangeName, + subjectSendToken, + subjectSendQuantity, + subjectReceiveToken, + subjectMinReceiveQuantity, + subjectData, + subjectCallerAddress, + subjectTransactionOptions + ); + } + + it('should call `tradeWithOperator` on the TradeExtensionWrapper', async () => { + await subject(); + + expect(tradeExtensionWrapper.tradeWithOperator).to.have.beenCalledWith( + subjectSetToken, + subjectExchangeName, + subjectSendToken, + subjectSendQuantity, + subjectReceiveToken, + subjectMinReceiveQuantity, + subjectData, + subjectCallerAddress, + subjectTransactionOptions + ); + }); + + describe('when a setToken is not a valid address', () => { + beforeEach(() => subjectSetToken = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when a exchangeName is not a valid string', () => { + beforeEach(() => subjectExchangeName = 5 as string); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when a sendToken is not a valid address', () => { + beforeEach(() => subjectSendToken = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when sendQuantity is not a valid number', () => { + beforeEach(() => subjectSendQuantity = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when a receiveToken is not a valid address', () => { + beforeEach(() => subjectReceiveToken = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + + describe('when minReceiveQuantity is not a valid number', () => { + beforeEach(() => subjectMinReceiveQuantity = NaN as BigNumber); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); + + describe('#getTradeExtensionInitializationBytecode', () => { + let subjectDelegatedManager: Address; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager; + }); + + async function subject(): Promise { + return tradeExtensionAPI.getTradeExtensionInitializationBytecode( + subjectDelegatedManager + ); + } + + it('should generate the expected bytecode', async () => { + const expectedBytecode = '0xde2236bd000000000000000000000000e36ea790bc9d7ab70c55260c66d52b1eca985f84'; + expect(await subject()).eq(expectedBytecode); + }); + + describe('when delegatedManager is not a valid address', () => { + beforeEach(() => subjectDelegatedManager = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); + + describe('#getTradeModuleAndExtensionInitializationBytecode', () => { + let subjectDelegatedManager: Address; + + beforeEach(async () => { + subjectDelegatedManager = delegatedManager; + }); + + async function subject(): Promise { + return tradeExtensionAPI.getTradeModuleAndExtensionInitializationBytecode(subjectDelegatedManager); + } + + it('should generate the expected bytecode', async () => { + const expectedBytecode = '0x9b468312000000000000000000000000e36ea790bc9d7ab70c55260c66d52b1eca985f84'; + + expect(await subject()).eq(expectedBytecode); + }); + + describe('when setToken is not a valid address', () => { + beforeEach(() => subjectDelegatedManager = '0xinvalid'); + + it('should throw with invalid params', async () => { + await expect(subject()).to.be.rejectedWith('Validation error'); + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index b52a7fa..008a735 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1016,10 +1016,22 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92" integrity sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q== -"@setprotocol/set-protocol-v2@^0.1.10": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@setprotocol/set-protocol-v2/-/set-protocol-v2-0.1.10.tgz#a8135e3e57cbdb857392bcf3cfe98d23bdc91333" - integrity sha512-PoDRg+ZeNk7eATvfqPxe1yMLTFoIgAfGA8oUmC8FTIM7F5Ya6wJCpzoJFaLcbd3lWK9+GUkoh6D1onLCatU5hw== +"@setprotocol/set-protocol-v2@^0.1.11-hardhat.0": + version "0.1.12" + resolved "https://registry.yarnpkg.com/@setprotocol/set-protocol-v2/-/set-protocol-v2-0.1.12.tgz#4ff67d0d2935148908ddada79bc51fda17c97afa" + integrity sha512-0zHRCTgASXR4P8ya0lafF4WSjnVsc6N7QMLiOGymVfuoSJDpDD+jGOn57FnVmgeknmt60frxJYMxxn5XQN9lyw== + dependencies: + "@uniswap/v3-sdk" "^3.5.1" + ethers "^5.5.2" + fs-extra "^5.0.0" + jsbi "^3.2.5" + module-alias "^2.2.2" + replace-in-file "^6.1.0" + +"@setprotocol/set-protocol-v2@^0.1.15": + version "0.1.15" + resolved "https://registry.yarnpkg.com/@setprotocol/set-protocol-v2/-/set-protocol-v2-0.1.15.tgz#d4c4ce5fe35ba136d045919635ea6abdf95489ba" + integrity sha512-XZxHXgGqKSk1SMlSeT7lA14nrfvjQ24xc+D+RpiZ/Qj6OAvuceFJ1rDGH234cTg3j3c7gUdAAe2XZ7hgVUenFA== dependencies: "@uniswap/v3-sdk" "^3.5.1" ethers "^5.5.2" @@ -1028,6 +1040,19 @@ module-alias "^2.2.2" replace-in-file "^6.1.0" +"@setprotocol/set-v2-strategies@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@setprotocol/set-v2-strategies/-/set-v2-strategies-0.0.7.tgz#dca84f40c31b7118b6ff527f372d7fe2906f53ad" + integrity sha512-YfA1obWvj2v/lsNnwrCHf2wRud+mdUZutHJggZyziqMokFi6dsej9StczSt8ftWUsWQqQnnGshVcBAjSL0T1sQ== + dependencies: + "@setprotocol/set-protocol-v2" "^0.1.11-hardhat.0" + "@uniswap/v3-sdk" "^3.5.1" + ethers "5.5.2" + fs-extra "^5.0.0" + jsbi "^3.2.5" + module-alias "^2.2.2" + replace-in-file "^6.1.0" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"