Skip to content

Commit

Permalink
Merge pull request #228 from Uniswap/compliance-provider
Browse files Browse the repository at this point in the history
feat: add filler compliance provider
  • Loading branch information
ConjunctiveNormalForm authored Nov 8, 2023
2 parents 828cbf1 + a3370e0 commit a4f202d
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 13 deletions.
2 changes: 2 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export const COMPLIANCE_CONFIG_BUCKET = 'compliance-config';
export const WEBHOOK_CONFIG_BUCKET = 'rfq-config';
export const SYNTH_SWITCH_BUCKET = 'synth-config';
export const FADE_RATE_BUCKET = 'fade-rate-config';
export const INTEGRATION_S3_KEY = 'integration.json';
export const PRODUCTION_S3_KEY = 'production.json';
export const BETA_S3_KEY = 'beta.json';
export const FADE_RATE_S3_KEY = 'fade-rate.json';
export const PROD_COMPLIANCE_S3_KEY = 'production.json';

export const DYNAMO_TABLE_NAME = {
FADES: 'Fades',
Expand Down
19 changes: 17 additions & 2 deletions lib/handlers/quote/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { default as bunyan, default as Logger } from 'bunyan';

import {
BETA_S3_KEY,
COMPLIANCE_CONFIG_BUCKET,
FADE_RATE_BUCKET,
FADE_RATE_S3_KEY,
INTEGRATION_S3_KEY,
PROD_COMPLIANCE_S3_KEY,
PRODUCTION_S3_KEY,
WEBHOOK_CONFIG_BUCKET,
} from '../../constants';
Expand All @@ -18,6 +20,7 @@ import {
} from '../../entities/aws-metrics-logger';
import { S3WebhookConfigurationProvider } from '../../providers';
import { S3CircuitBreakerConfigurationProvider } from '../../providers/circuit-breaker/s3';
import { S3FillerComplianceConfigurationProvider } from '../../providers/compliance/s3';
import { Quoter, WebhookQuoter } from '../../quoters';
import { STAGE } from '../../util/stage';
import { ApiInjector, ApiRInj } from '../base/api-handler';
Expand Down Expand Up @@ -48,7 +51,14 @@ export class QuoteInjector extends ApiInjector<ContainerInjected, RequestInjecte
`${FADE_RATE_BUCKET}-${stage}-1`,
FADE_RATE_S3_KEY
);
const quoters: Quoter[] = [new WebhookQuoter(log, webhookProvider, circuitBreakerProvider)];

const fillerComplianceProvider = new S3FillerComplianceConfigurationProvider(
log,
`${COMPLIANCE_CONFIG_BUCKET}-${stage}-1`,
PROD_COMPLIANCE_S3_KEY
);

const quoters: Quoter[] = [new WebhookQuoter(log, webhookProvider, circuitBreakerProvider, fillerComplianceProvider)];
return {
quoters: quoters,
};
Expand Down Expand Up @@ -104,7 +114,12 @@ export class MockQuoteInjector extends ApiInjector<ContainerInjected, RequestInj
`${FADE_RATE_BUCKET}-${stage}-1`,
FADE_RATE_S3_KEY
);
const quoters: Quoter[] = [new WebhookQuoter(log, webhookProvider, circuitBreakerProvider)];
const fillerComplianceProvider = new S3FillerComplianceConfigurationProvider(
log,
`${COMPLIANCE_CONFIG_BUCKET}-${stage}-1`,
PROD_COMPLIANCE_S3_KEY
);
const quoters: Quoter[] = [new WebhookQuoter(log, webhookProvider, circuitBreakerProvider, fillerComplianceProvider)];

return {
quoters: quoters,
Expand Down
13 changes: 13 additions & 0 deletions lib/providers/compliance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface FillerComplianceConfiguration {
endpoints: string[];
addresses: string[];
}

export interface FillerComplianceConfigurationProvider {
getConfigs(): Promise<FillerComplianceConfiguration[]>;
// getExcludedAddrToEndpointsMap(): Promise<Map<string, Set<string>>>;
getEndpointToExcludedAddrsMap(): Promise<Map<string, Set<string>>>;
}

export * from './mock';
export * from './s3';
25 changes: 25 additions & 0 deletions lib/providers/compliance/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FillerComplianceConfiguration, FillerComplianceConfigurationProvider } from '.';

export class MockFillerComplianceConfigurationProvider implements FillerComplianceConfigurationProvider {
constructor(private configs: FillerComplianceConfiguration[]) {}

async getConfigs(): Promise<FillerComplianceConfiguration[]> {
return this.configs;
}

async getEndpointToExcludedAddrsMap(): Promise<Map<string, Set<string>>> {
const map = new Map<string, Set<string>>();
this.configs.forEach((config) => {
config.endpoints.forEach((endpoint) => {
if (!map.has(endpoint)) {
map.set(endpoint, new Set<string>());
}
config.addresses.forEach((address) => {
map.get(endpoint)?.add(address);
});
});
})
return map;
}

}
60 changes: 60 additions & 0 deletions lib/providers/compliance/s3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { default as Logger } from 'bunyan';

import { checkDefined } from '../../preconditions/preconditions';
import { FillerComplianceConfiguration, FillerComplianceConfigurationProvider } from '.';


export class S3FillerComplianceConfigurationProvider implements FillerComplianceConfigurationProvider {
private log: Logger;
private configs: FillerComplianceConfiguration[];
private endpointToExcludedAddrsMap: Map<string, Set<string>>;

constructor(_log: Logger, private bucket: string, private key: string) {
this.configs = [];
this.log = _log.child({ quoter: 'S3FillerComplianceConfigurationProvider' });
this.endpointToExcludedAddrsMap = new Map<string, Set<string>>();
}
async getEndpointToExcludedAddrsMap(): Promise<Map<string, Set<string>>> {
if (this.configs.length === 0) {
await this.fetchConfigs();
}
if (this.endpointToExcludedAddrsMap.size > 0) {
return this.endpointToExcludedAddrsMap;
}
this.configs.forEach((config) => {
config.endpoints.forEach((endpoint) => {
if (!this.endpointToExcludedAddrsMap.has(endpoint)) {
this.endpointToExcludedAddrsMap.set(endpoint, new Set<string>());
}
config.addresses.forEach((address) => {
this.endpointToExcludedAddrsMap.get(endpoint)?.add(address);
});
});
})
return this.endpointToExcludedAddrsMap;
}

async getConfigs(): Promise<FillerComplianceConfiguration[]> {
if (
this.configs.length === 0
) {
await this.fetchConfigs();
}
return this.configs;
}

async fetchConfigs(): Promise<void> {
const s3Client = new S3Client({});
const s3Res = await s3Client.send(
new GetObjectCommand({
Bucket: this.bucket,
Key: this.key,
})
);
const s3Body = checkDefined(s3Res.Body, 's3Res.Body is undefined');
this.configs = JSON.parse(await s3Body.transformToString()) as FillerComplianceConfiguration[];
this.log.info({ configsLength: this.configs.map((c) => c.addresses.length) }, `Fetched configs`);
}
}
8 changes: 8 additions & 0 deletions lib/quoters/WebhookQuoter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
import { Metric, metricContext, QuoteRequest, QuoteResponse } from '../entities';
import { WebhookConfiguration, WebhookConfigurationProvider } from '../providers';
import { CircuitBreakerConfigurationProvider } from '../providers/circuit-breaker';
import { FillerComplianceConfigurationProvider } from '../providers/compliance';
import { Quoter, QuoterType } from '.';

// TODO: shorten, maybe take from env config
Expand All @@ -22,6 +23,7 @@ export class WebhookQuoter implements Quoter {
_log: Logger,
private webhookProvider: WebhookConfigurationProvider,
private circuitBreakerProvider: CircuitBreakerConfigurationProvider,
private complianceProvider: FillerComplianceConfigurationProvider,
_allow_list: Set<string> = new Set<string>(['9de8f2376fef4be567f2e242fce750cca347b71853816cbc64f70d568de41ef1'])
) {
this.log = _log.child({ quoter: 'WebhookQuoter' });
Expand All @@ -30,6 +32,12 @@ export class WebhookQuoter implements Quoter {

public async quote(request: QuoteRequest): Promise<QuoteResponse[]> {
const endpoints = await this.getEligibleEndpoints();
const endpointToAddrsMap = await this.complianceProvider.getEndpointToExcludedAddrsMap();
endpoints.filter((e) => {
return endpointToAddrsMap.get(e.endpoint) === undefined ||
!endpointToAddrsMap.get(e.endpoint)?.has(request.swapper);
});

this.log.info({ endpoints }, `Fetching quotes from ${endpoints.length} endpoints`);
const quotes = await Promise.all(endpoints.map((e) => this.fetchQuote(e, request)));
return quotes.filter((q) => q !== null) as QuoteResponse[];
Expand Down
48 changes: 41 additions & 7 deletions test/handlers/quote/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { QuoteHandler } from '../../../lib/handlers/quote/handler';
import { MockWebhookConfigurationProvider } from '../../../lib/providers';
import { MockCircuitBreakerConfigurationProvider } from '../../../lib/providers/circuit-breaker/mock';
import { MockFillerComplianceConfigurationProvider } from '../../../lib/providers/compliance';
import { MOCK_FILLER_ADDRESS, MockQuoter, Quoter, WebhookQuoter } from '../../../lib/quoters';

jest.mock('axios');
Expand All @@ -31,6 +32,12 @@ const CHAIN_ID = 1;
const logger = Logger.createLogger({ name: 'test' });
logger.level(Logger.FATAL);

const emptyMockComplianceProvider = new MockFillerComplianceConfigurationProvider([]);
const mockComplianceProvider = new MockFillerComplianceConfigurationProvider([{
endpoints: ['https://uniswap.org', 'google.com'], addresses: [SWAPPER]
}]);


describe('Quote handler', () => {
// Creating mocks for all the handler dependencies.
const requestInjectedMock: Promise<RequestInjected> = new Promise(
Expand Down Expand Up @@ -212,7 +219,7 @@ describe('Quote handler', () => {
const circuitBreakerProvider = new MockCircuitBreakerConfigurationProvider([
{ fadeRate: 0.02, enabled: true, hash: '0xuni' },
]);
const quoters = [new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider)];
const quoters = [new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider, emptyMockComplianceProvider)];
const amountIn = ethers.utils.parseEther('1');
const request = getRequest(amountIn.toString());

Expand Down Expand Up @@ -276,7 +283,7 @@ describe('Quote handler', () => {
const circuitBreakerProvider = new MockCircuitBreakerConfigurationProvider([
{ hash: '0xuni', fadeRate: 0.02, enabled: true },
]);
const quoters = [new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider)];
const quoters = [new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider, emptyMockComplianceProvider)];
const amountIn = ethers.utils.parseEther('1');
const request = getRequest(amountIn.toString());

Expand Down Expand Up @@ -322,7 +329,7 @@ describe('Quote handler', () => {
const circuitBreakerProvider = new MockCircuitBreakerConfigurationProvider([
{ hash: '0xuni', fadeRate: 0.02, enabled: true },
]);
const quoters = [new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider)];
const quoters = [new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider, emptyMockComplianceProvider)];
const amountIn = ethers.utils.parseEther('1');
const request = getRequest(amountIn.toString());

Expand All @@ -348,7 +355,7 @@ describe('Quote handler', () => {
const circuitBreakerProvider = new MockCircuitBreakerConfigurationProvider([
{ hash: '0xuni', fadeRate: 0.02, enabled: true },
]);
const quoters = [new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider)];
const quoters = [new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider, emptyMockComplianceProvider)];
const amountIn = ethers.utils.parseEther('1');
const request = getRequest(amountIn.toString());

Expand Down Expand Up @@ -376,7 +383,7 @@ describe('Quote handler', () => {
{ hash: '0xuni', fadeRate: 0.02, enabled: true },
]);
const quoters = [
new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider),
new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider, emptyMockComplianceProvider),
new MockQuoter(logger, 1, 1),
];
const amountIn = ethers.utils.parseEther('1');
Expand Down Expand Up @@ -408,7 +415,7 @@ describe('Quote handler', () => {
{ hash: '0xuni', fadeRate: 0.02, enabled: true },
]);
const quoters = [
new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider),
new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider, emptyMockComplianceProvider),
new MockQuoter(logger, 1, 1),
];
const amountIn = ethers.utils.parseEther('1');
Expand Down Expand Up @@ -462,7 +469,7 @@ describe('Quote handler', () => {
{ hash: '0xuni', fadeRate: 0.02, enabled: true },
]);
const quoters = [
new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider),
new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider, emptyMockComplianceProvider),
new MockQuoter(logger, 1, 1),
];
const amountIn = ethers.utils.parseEther('1');
Expand Down Expand Up @@ -494,5 +501,32 @@ describe('Quote handler', () => {
quoteId: expect.any(String),
});
});

it('respects filler compliance requirements', async () => {
const webhookProvider = new MockWebhookConfigurationProvider([
{ name: 'uniswap', endpoint: 'https://uniswap.org', headers: {}, hash: '0xuni' },
]);
const circuitBreakerProvider = new MockCircuitBreakerConfigurationProvider([
{ hash: '0xuni', fadeRate: 0.02, enabled: true },
]);
const quoters = [
new WebhookQuoter(logger, webhookProvider, circuitBreakerProvider, mockComplianceProvider),
];
const amountIn = ethers.utils.parseEther('1');
const request = getRequest(amountIn.toString());

const response: APIGatewayProxyResult = await getQuoteHandler(quoters).handler(
getEvent(request),
{} as unknown as Context
);
expect(response.statusCode).toEqual(404);
const quoteResponse: PostQuoteResponse = JSON.parse(response.body);
expect(quoteResponse).toMatchObject(
expect.objectContaining({
errorCode: 'QUOTE_ERROR',
detail: 'No quotes available',
})
)
})
});
});
59 changes: 59 additions & 0 deletions test/providers/compliance/s3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { S3Client } from '@aws-sdk/client-s3';
import { default as Logger } from 'bunyan';

import { FillerComplianceConfiguration,S3FillerComplianceConfigurationProvider } from '../../../lib/providers/compliance';

const mockConfigs = [
{
endpoints: ['https://google.com'],
addresses: ['0x1234'],
},
{
endpoints: ['https://meta.com'],
addresses: ['0x1234', '0x5678'],
},
];


function applyMock(configs: FillerComplianceConfiguration[]) {
jest.spyOn(S3Client.prototype, 'send').mockImplementationOnce(() =>
Promise.resolve({
Body: {
transformToString: () => Promise.resolve(JSON.stringify(configs)),
},
})
);
}


// silent logger in tests
const logger = Logger.createLogger({ name: 'test' });
logger.level(Logger.FATAL);

describe('S3ComplianceConfigurationProvider', () => {
const bucket = 'test-bucket';
const key = 'test-key';

afterEach(() => {
jest.clearAllMocks();
});

it('fetches configs', async () => {
applyMock(mockConfigs);
const provider = new S3FillerComplianceConfigurationProvider(logger, bucket, key);
const endpoints = await provider.getConfigs();
expect(endpoints).toEqual(mockConfigs);
});

it('generates endpoint to addrs map', async () => {
applyMock(mockConfigs);
const provider = new S3FillerComplianceConfigurationProvider(logger, bucket, key);
const map = await provider.getEndpointToExcludedAddrsMap();
expect(map).toMatchObject(
new Map([
['https://google.com', new Set(['0x1234'])],
['https://meta.com', new Set(['0x1234', '0x5678'])],
])
)
});
});
Loading

0 comments on commit a4f202d

Please sign in to comment.