Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(twap): twap order creation services #2504

Merged
merged 5 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/abis/ComposableCoW.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[
{
"inputs": [
{
"components": [
{
"internalType": "contract IConditionalOrder",
"name": "handler",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "staticInput",
"type": "bytes"
}
],
"internalType": "struct IConditionalOrder.ConditionalOrderParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bool",
"name": "dispatch",
"type": "bool"
}
],
"name": "create",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
148 changes: 148 additions & 0 deletions src/abis/ExtensibleFallbackHandler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_defaultFallbackHandler",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "contract Safe",
"name": "safe",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes4",
"name": "selector",
"type": "bytes4"
},
{
"indexed": false,
"internalType": "contract IFallbackMethod",
"name": "handler",
"type": "address"
}
],
"name": "AddedSafeMethod",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "contract Safe",
"name": "safe",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "oldHandler",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "newHandler",
"type": "address"
}
],
"name": "ChangedDefaultFallbackHandler",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "contract Safe",
"name": "safe",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes4",
"name": "selector",
"type": "bytes4"
},
{
"indexed": false,
"internalType": "contract IFallbackMethod",
"name": "oldHandler",
"type": "address"
},
{
"indexed": false,
"internalType": "contract IFallbackMethod",
"name": "newHandler",
"type": "address"
}
],
"name": "ChangedSafeMethod",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "contract Safe",
"name": "safe",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes4",
"name": "selector",
"type": "bytes4"
}
],
"name": "RemovedSafeMethod",
"type": "event"
},
{
"stateMutability": "nonpayable",
"type": "fallback"
},
{
"inputs": [
{
"internalType": "address",
"name": "newHandler",
"type": "address"
}
],
"name": "setDefaultFallbackHandler",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "selector",
"type": "bytes4"
},
{
"internalType": "contract IFallbackMethod",
"name": "newHandler",
"type": "address"
}
],
"name": "setSafeMethod",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
7 changes: 7 additions & 0 deletions src/modules/advancedOrders/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk'

export const COMPOSABLE_COW_ADDRESS: Record<SupportedChainId, string> = {
1: 'TODO',
100: 'TODO',
5: '0xa31b99bd44528c7bae9e1f675d810ae13b0e29aa',
}
10 changes: 10 additions & 0 deletions src/modules/advancedOrders/hooks/useComposableCowContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ComposableCoW } from 'abis/types'
import { useWalletInfo } from 'modules/wallet'
import COMPOSABLE_COW_ABI from 'abis/ComposableCoW.json'
import { useContract } from 'legacy/hooks/useContract'
import { COMPOSABLE_COW_ADDRESS } from '../const'

export function useComposableCowContract(): ComposableCoW | null {
const { chainId } = useWalletInfo()
return useContract<ComposableCoW>(chainId ? COMPOSABLE_COW_ADDRESS[chainId] : undefined, COMPOSABLE_COW_ABI, true)
}
7 changes: 7 additions & 0 deletions src/modules/twap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# TWAP orders

## Twap order creation

The process follows common pattern: collect context -> execute logic

![twap-creation](./docs/twap-creation.drawio.svg)
10 changes: 10 additions & 0 deletions src/modules/twap/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk'

export const TWAP_ORDER_STRUCT =
'tuple(address sellToken,address buyToken,address receiver,uint256 partSellAmount,uint256 minPartLimit,uint256 t0,uint256 n,uint256 t,uint256 span)'

export const TWAP_HANDLER_ADDRESS: Record<SupportedChainId, string> = {
1: 'TODO',
100: 'TODO',
5: '0xa12d770028d7072b80baeb6a1df962374fd13d9a',
}
4 changes: 4 additions & 0 deletions src/modules/twap/docs/twap-creation.drawio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions src/modules/twap/hooks/useTwapOrderCreationContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useComposableCowContract } from 'modules/advancedOrders/hooks/useComposableCowContract'
import { useNeedsApproval } from 'common/hooks/useNeedsApproval'
import { useTokenContract } from 'legacy/hooks/useContract'
import { ComposableCoW } from 'abis/types'
import { Erc20 } from 'legacy/abis/types'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { Nullish } from 'types'
import { useWalletInfo } from 'modules/wallet'
import { useSafeAppsSdk } from 'modules/wallet/web3-react/hooks/useSafeAppsSdk'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import SafeAppsSDK from '@safe-global/safe-apps-sdk'
import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress'

export interface TwapOrderCreationContext {
chainId: SupportedChainId
safeAppsSdk: SafeAppsSDK
composableCowContract: ComposableCoW
needsApproval: boolean
spender: string
erc20Contract: Erc20
}

export function useTwapOrderCreationContext(
inputAmount: Nullish<CurrencyAmount<Token>>
): TwapOrderCreationContext | null {
const { chainId } = useWalletInfo()
const safeAppsSdk = useSafeAppsSdk()
const composableCowContract = useComposableCowContract()
const needsApproval = useNeedsApproval(inputAmount)
const erc20Contract = useTokenContract(inputAmount?.currency.address)
const spender = useTradeSpenderAddress()

if (!composableCowContract || !erc20Contract || !chainId || !safeAppsSdk || !spender) return null

return { chainId, safeAppsSdk, composableCowContract, erc20Contract, needsApproval, spender }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Create TWAP order When sell token is NOT approved, then should generate approval and creation transactions 1`] = `
Array [
"create",
Array [
Object {
"handler": "0xa12d770028d7072b80baeb6a1df962374fd13d9a",
"salt": "0x00000000000000000000000000000000000000000000000000000015c90b9b2a",
"staticInput": "0x00000000000000000000000091056d4a53e1faa1a84306d4deaec71085394bc8000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000",
},
true,
],
]
`;

exports[`Create TWAP order When sell token is NOT approved, then should generate approval and creation transactions 2`] = `
Array [
"approve",
Array [
"0xB4FBF271143F4FBf7B91A5ded31805e42b222222",
"100000000000",
],
]
`;

exports[`Create TWAP order When sell token is NOT approved, then should generate approval and creation transactions 3`] = `
Array [
Object {
"data": "0xAPPROVE_TX_DATA",
"operation": 0,
"to": "0x91056D4A53E1faa1A84306D4deAEc71085394bC8",
"value": "0",
},
Object {
"data": "0xCREATE_COW_TX_DATA",
"operation": 0,
"to": "0xa31b99bd44528c7bae9e1f675d810ae13b0e29aa",
"value": "0",
},
]
`;

exports[`Create TWAP order When sell token is approved, then should generate only creation transaction 1`] = `
Array [
"create",
Array [
Object {
"handler": "0xa12d770028d7072b80baeb6a1df962374fd13d9a",
"salt": "0x00000000000000000000000000000000000000000000000000000015c90b9b2a",
"staticInput": "0x00000000000000000000000091056d4a53e1faa1a84306d4deaec71085394bc8000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d600000000000000000000000000000000000000000000000000000007c2d24d55000000000000000000000000000000000000000000000000000000000001046a00000000000000000000000000000000000000000000000000000000646b782c000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000",
},
true,
],
]
`;

exports[`Create TWAP order When sell token is approved, then should generate only creation transaction 2`] = `
Array [
Object {
"data": "0xCREATE_COW_TX_DATA",
"operation": 0,
"to": "0xa31b99bd44528c7bae9e1f675d810ae13b0e29aa",
"value": "0",
},
]
`;
65 changes: 65 additions & 0 deletions src/modules/twap/services/createTwapOrderTxs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createTwapOrderTxs } from './createTwapOrderTxs'
import { TWAPOrder } from '../types'
import { CurrencyAmount } from '@uniswap/sdk-core'
import { COW } from '../../../legacy/constants/tokens'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { WETH_GOERLI } from '../../../legacy/utils/goerli/constants'
import { TwapOrderCreationContext } from '../hooks/useTwapOrderCreationContext'

const order: TWAPOrder = {
sellAmount: CurrencyAmount.fromRawAmount(COW[SupportedChainId.GOERLI], 100_000_000_000),
buyAmount: CurrencyAmount.fromRawAmount(WETH_GOERLI, 200_000),
receiver: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
numOfParts: 3,
startTime: 1684764716,
timeInterval: 600,
span: 0,
}

const CREATE_COW_TX_DATA = '0xCREATE_COW_TX_DATA'
const APPROVE_TX_DATA = '0xAPPROVE_TX_DATA'

describe('Create TWAP order', () => {
let context: TwapOrderCreationContext
let createCowFn: jest.Mock
let approveFn: jest.Mock

beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => 1497076708000)

createCowFn = jest.fn().mockReturnValue(CREATE_COW_TX_DATA)
approveFn = jest.fn().mockReturnValue(APPROVE_TX_DATA)

context = {
chainId: SupportedChainId.GOERLI,
safeAppsSdk: null as any,
composableCowContract: { interface: { encodeFunctionData: createCowFn } } as any,
needsApproval: false,
spender: '0xB4FBF271143F4FBf7B91A5ded31805e42b222222',
erc20Contract: { interface: { encodeFunctionData: approveFn } } as any,
}
})

it('When sell token is approved, then should generate only creation transaction', () => {
const result = createTwapOrderTxs(order, { ...context, needsApproval: false })

expect(createCowFn).toHaveBeenCalledTimes(1)
expect(createCowFn.mock.calls[0]).toMatchSnapshot()

expect(result.length).toBe(1)
expect(result).toMatchSnapshot()
})

it('When sell token is NOT approved, then should generate approval and creation transactions', () => {
const result = createTwapOrderTxs(order, { ...context, needsApproval: true })

expect(createCowFn).toHaveBeenCalledTimes(1)
expect(createCowFn.mock.calls[0]).toMatchSnapshot()

expect(approveFn).toHaveBeenCalledTimes(1)
expect(approveFn.mock.calls[0]).toMatchSnapshot()

expect(result.length).toBe(2)
expect(result).toMatchSnapshot()
})
})
Loading