Skip to content

Commit

Permalink
[OTE-626]: Add support for whitelisted addresses (dydxprotocol#1966)
Browse files Browse the repository at this point in the history
  • Loading branch information
Christopher-Li authored Jul 25, 2024
1 parent 5a6eed7 commit f162a11
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
isRestrictedCountryHeaders,
CountryHeaders,
isWhitelistedAddress,
} from '../../src/geoblocking/restrict-countries';
import * as util from '../../src/geoblocking/util';
import config from '../../src/config';
Expand Down Expand Up @@ -47,3 +48,17 @@ describe('isRestrictedCountryHeaders', () => {
expect(isRestrictedCountryHeaders({})).toEqual(true);
});
});

describe('isWhitelistedAddress', () => {
it('returns true if address is whitelisted', () => {
config.WHITELISTED_ADDRESSES = '0x123,0x456';

expect(isWhitelistedAddress('0x123')).toEqual(true);
});

it('returns false if address is not whitelisted', () => {
config.WHITELISTED_ADDRESSES = '0x123,0x456';

expect(isWhitelistedAddress('0x789')).toEqual(false);
});
});
5 changes: 5 additions & 0 deletions indexer/packages/compliance/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export const complianceConfigSchema = {
default: '', // comma de-limited
}),

// Whitelisted list of dydx addresses
WHITELISTED_ADDRESSES: parseString({
default: '', // comma de-limited
}),

// Required environment variables.
ELLIPTIC_API_KEY: parseString({ default: 'default_elliptic_api_key' }),
ELLIPTIC_API_SECRET: parseString({ default: '' }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ export function isRestrictedCountryHeaders(headers: CountryHeaders): boolean {

return false;
}

export function isWhitelistedAddress(address: string): boolean {
return config.WHITELISTED_ADDRESSES.split(',').includes(address);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { DateTime } from 'luxon';
import { ComplianceAction } from '../../../../src/controllers/api/v4/compliance-v2-controller';
import { ExtendedSecp256k1Signature, Secp256k1 } from '@cosmjs/crypto';
import { getGeoComplianceReason } from '../../../../src/helpers/compliance/compliance-utils';
import { isRestrictedCountryHeaders } from '@dydxprotocol-indexer/compliance';
import { isRestrictedCountryHeaders, isWhitelistedAddress } from '@dydxprotocol-indexer/compliance';
import { toBech32 } from '@cosmjs/encoding';

jest.mock('@dydxprotocol-indexer/compliance');
Expand Down Expand Up @@ -60,8 +60,11 @@ describe('ComplianceV2Controller', () => {
});

describe('GET', () => {
let isWhitelistedAddressSpy: jest.SpyInstance;

beforeEach(async () => {
ipAddrMock.mockReturnValue(ipAddr);
isWhitelistedAddressSpy = isWhitelistedAddress as unknown as jest.Mock;
await testMocks.seedData();
});

Expand Down Expand Up @@ -158,6 +161,31 @@ describe('ComplianceV2Controller', () => {
}));
});

it('should return COMPLIANT for a restricted, dydx address with existing CLOSE_ONLY compliance status', async () => {
jest.spyOn(ComplianceControllerHelper.prototype, 'screen').mockImplementation(() => {
return Promise.resolve({
restricted: true,
});
});

const createdAt: string = DateTime.utc().minus({ days: 1 }).toISO();
await ComplianceStatusTable.create({
address: testConstants.defaultAddress,
status: ComplianceStatus.CLOSE_ONLY,
createdAt,
updatedAt: createdAt,
});
const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(1);

isWhitelistedAddressSpy.mockReturnValue(true);
const response: any = await sendRequest({
type: RequestMethod.GET,
path: `/v4/compliance/screen/${testConstants.defaultAddress}`,
});
expect(response.body.status).toEqual(ComplianceStatus.COMPLIANT);
});

it('should return CLOSE_ONLY & not update for a restricted, dydx address with existing CLOSE_ONLY compliance status',
async () => {
jest.spyOn(ComplianceControllerHelper.prototype, 'screen').mockImplementation(() => {
Expand Down Expand Up @@ -190,7 +218,8 @@ describe('ComplianceV2Controller', () => {
createdAt,
updatedAt: createdAt,
}));
});
},
);

it('should return COMPLIANT for a non-restricted, dydx address', async () => {
jest.spyOn(ComplianceControllerHelper.prototype, 'screen').mockImplementation(() => {
Expand Down Expand Up @@ -298,6 +327,7 @@ describe('ComplianceV2Controller', () => {
describe('POST /geoblock', () => {
let getGeoComplianceReasonSpy: jest.SpyInstance;
let isRestrictedCountryHeadersSpy: jest.SpyInstance;
let isWhitelistedAddressSpy: jest.SpyInstance;

const body: any = {
address: testConstants.defaultAddress,
Expand All @@ -311,6 +341,7 @@ describe('ComplianceV2Controller', () => {
beforeEach(async () => {
getGeoComplianceReasonSpy = getGeoComplianceReason as unknown as jest.Mock;
isRestrictedCountryHeadersSpy = isRestrictedCountryHeaders as unknown as jest.Mock;
isWhitelistedAddressSpy = isWhitelistedAddress as unknown as jest.Mock;
ipAddrMock.mockReturnValue(ipAddr);
await testMocks.seedData();
jest.mock('@cosmjs/crypto', () => ({
Expand Down Expand Up @@ -392,6 +423,30 @@ describe('ComplianceV2Controller', () => {
expect(response.body.status).toEqual(ComplianceStatus.COMPLIANT);
});

it('should return COMPLIANT from a restricted country when whitelisted', async () => {
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
getGeoComplianceReasonSpy.mockReturnValueOnce(ComplianceReason.US_GEO);
isRestrictedCountryHeadersSpy.mockReturnValue(true);
await dbHelpers.clearData();

const data2: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data2).toHaveLength(0);

isWhitelistedAddressSpy.mockReturnValue(true);
const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body,
expectedStatus: 200,
});

const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(0);

expect(response.body.status).toEqual(ComplianceStatus.COMPLIANT);
expect(response.body.updatedAt).toBeDefined();
});

it('should set status to BLOCKED for CONNECT action from a restricted country with no existing compliance status and no wallet', async () => {
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
getGeoComplianceReasonSpy.mockReturnValueOnce(ComplianceReason.US_GEO);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
INDEXER_COMPLIANCE_BLOCKED_PAYLOAD,
INDEXER_GEOBLOCKED_PAYLOAD,
isRestrictedCountryHeaders,
isWhitelistedAddress,
} from '@dydxprotocol-indexer/compliance';
import config from '../../src/config';

Expand Down Expand Up @@ -69,6 +70,7 @@ export const complianceCheckApp = Server(router);

describe('compliance-check', () => {
let isRestrictedCountrySpy: jest.SpyInstance;
let isWhitelistedAddressSpy: jest.SpyInstance;

beforeAll(async () => {
config.INDEXER_LEVEL_GEOBLOCKING_ENABLED = true;
Expand All @@ -77,6 +79,8 @@ describe('compliance-check', () => {

beforeEach(async () => {
isRestrictedCountrySpy = isRestrictedCountryHeaders as unknown as jest.Mock;
isWhitelistedAddressSpy = isWhitelistedAddress as jest.Mock;
isWhitelistedAddressSpy.mockReturnValue(false);
await testMocks.seedData();
});

Expand All @@ -86,6 +90,7 @@ describe('compliance-check', () => {

afterEach(async () => {
jest.restoreAllMocks();
config.WHITELISTED_ADDRESSES = '';
await dbHelpers.clearData();
});

Expand Down Expand Up @@ -233,6 +238,23 @@ describe('compliance-check', () => {
}));
});

it.each([
['query', `/v4/check-compliance-query?address=${testConstants.defaultAddress}`],
['param', `/v4/check-compliance-param/${testConstants.defaultAddress}`],
])('does not return 403 if address is whitelisted and request is from restricted country (%s)', async (
_name: string,
path: string,
) => {
isWhitelistedAddressSpy.mockReturnValue(true);
isRestrictedCountrySpy.mockReturnValueOnce(true);
await sendRequestToApp({
type: RequestMethod.GET,
path,
expressApp: complianceCheckApp,
expectedStatus: 200,
});
});

it.each([
['query', `/v4/check-compliance-query?address=${testConstants.blockedAddress}`],
['param', `/v4/check-compliance-param/${testConstants.blockedAddress}`],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
} from '@cosmjs/crypto';
import { toBech32 } from '@cosmjs/encoding';
import { logger, stats, TooManyRequestsError } from '@dydxprotocol-indexer/base';
import { CountryHeaders, isRestrictedCountryHeaders } from '@dydxprotocol-indexer/compliance';
import { CountryHeaders, isRestrictedCountryHeaders, isWhitelistedAddress } from '@dydxprotocol-indexer/compliance';
import {
ComplianceReason,
ComplianceStatus,
Expand Down Expand Up @@ -145,6 +145,11 @@ router.get(
}: {
address: string,
} = matchedData(req) as ComplianceRequest;
if (isWhitelistedAddress(address)) {
return res.send({
status: ComplianceStatus.COMPLIANT,
});
}

try {
// Rate limiter middleware ensures the ip address can be found from the request
Expand Down Expand Up @@ -252,6 +257,13 @@ router.post(
);
}

if (isWhitelistedAddress(address)) {
return res.send({
status: ComplianceStatus.COMPLIANT,
updatedAt: DateTime.utc().toISO(),
});
}

const [
complianceStatus,
wallet,
Expand Down
5 changes: 5 additions & 0 deletions indexer/services/comlink/src/lib/compliance-and-geo-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
isRestrictedCountryHeaders,
INDEXER_GEOBLOCKED_PAYLOAD,
INDEXER_COMPLIANCE_BLOCKED_PAYLOAD,
isWhitelistedAddress,
} from '@dydxprotocol-indexer/compliance';
import {
ComplianceStatus,
Expand Down Expand Up @@ -40,6 +41,10 @@ export async function complianceAndGeoCheck(
}

const { address }: AddressRequest = matchedData(req) as AddressRequest;
if (isWhitelistedAddress(address)) {
return next();
}

if (address !== undefined) {
const updatedStatus: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll(
{ address: [address] },
Expand Down

0 comments on commit f162a11

Please sign in to comment.