Skip to content

Commit

Permalink
Compliance + Test (#149)
Browse files Browse the repository at this point in the history
* add @celo/celocli -- when doing transfers or exchanges ensure that all addresses complied with OFAC sanctions

* add basic tests for transfers + compliance

* add dynamic list checking. sadly means knew dep but its already a transitative dep so ok


* fewer lines but same result, thanks nico

* latest compliance package for good measure

---------

Co-authored-by: Aaron <[email protected]>
  • Loading branch information
aaronmgdr and aaronmgdr authored Feb 22, 2024
1 parent 6157c6d commit ae51ca8
Show file tree
Hide file tree
Showing 16 changed files with 317 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-poems-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@celo/celocli': patch
---

Require addresses the cli sends from or to not to be sanctioned
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dependencies": {
"@celo/abis": "10.0.0",
"@celo/base": "^6.0.0",
"@celo/compliance": "~1.0.17",
"@celo/connect": "^5.1.2",
"@celo/contractkit": "^7.0.0",
"@celo/cryptographic-utils": "^5.0.7",
Expand All @@ -60,6 +61,7 @@
"bip32": "3.1.0",
"chalk": "^2.4.2",
"command-exists": "^1.2.9",
"cross-fetch": "3.0.6",
"debug": "^4.1.1",
"ethers": "5",
"fs-extra": "^8.1.0",
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/commands/exchange/celo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ export default class ExchangeCelo extends BaseCommand {
const minBuyAmount = res.flags.forAtLeast
const stableToken = stableTokenOptions[res.flags.stableToken]

await newCheckBuilder(this).hasEnoughCelo(res.flags.from, sellAmount).runChecks()
await newCheckBuilder(this)
.isNotSanctioned(res.flags.from)
.hasEnoughCelo(res.flags.from, sellAmount)
.runChecks()

const [celoToken, stableTokenAddress, { mento, brokerAddress }] = await Promise.all([
kit.contracts.getGoldToken(),
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/lockedgold/withdraw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default class Withdraw extends BaseCommand {
kit.defaultAccount = flags.from
const lockedgold = await kit.contracts.getLockedGold()

await newCheckBuilder(this).isAccount(flags.from).runChecks()
await newCheckBuilder(this).isAccount(flags.from).isNotSanctioned(flags.from).runChecks()

const currentTime = Math.round(new Date().getTime() / 1000)
let madeWithdrawal = false
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/releasecelo/transfer-dollars.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { newCheckBuilder } from '../../utils/checks'
import { displaySendTx } from '../../utils/cli'
import { CustomFlags } from '../../utils/command'
import { ReleaseGoldBaseCommand } from '../../utils/release-gold-base'
Expand Down Expand Up @@ -31,6 +32,7 @@ export default class TransferDollars extends ReleaseGoldBaseCommand {
kit.defaultAccount = isRevoked
? await this.releaseGoldWrapper.getReleaseOwner()
: await this.releaseGoldWrapper.getBeneficiary()
newCheckBuilder(this).isNotSanctioned(kit.defaultAccount).isNotSanctioned(flags.to).runChecks()
await displaySendTx('transfer', this.releaseGoldWrapper.transfer(flags.to, flags.value))
}
}
6 changes: 4 additions & 2 deletions packages/cli/src/commands/releasecelo/withdraw.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ testWithGanache('releasegold:withdraw cmd', (web3: Web3) => {
})

test("can't withdraw the whole balance if there is a cUSD balance", async () => {
const spy = jest.spyOn(console, 'log')
await testLocally(SetLiquidityProvision, ['--contract', contractAddress, '--yesreally'])
// ReleasePeriod of default contract
await timeTravel(300000000, web3)
Expand All @@ -65,7 +66,9 @@ testWithGanache('releasegold:withdraw cmd', (web3: Web3) => {
await expect(
testLocally(Withdraw, ['--contract', contractAddress, '--value', remainingBalance.toString()])
).rejects.toThrow()

expect(spy).toHaveBeenCalledWith(
expect.stringContaining('The liquidity provision has not already been set')
)
// Move out the cUSD balance
await testLocally(RGTransferDollars, [
'--contract',
Expand All @@ -84,7 +87,6 @@ testWithGanache('releasegold:withdraw cmd', (web3: Web3) => {
])
const balanceAfter = await kit.getTotalBalance(beneficiary)
expect(balanceBefore.CELO!.toNumber()).toBeLessThan(balanceAfter.CELO!.toNumber())

// Contract should self-destruct now
await expect(releaseGoldWrapper.getRemainingUnlockedBalance()).rejects.toThrow()
})
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/releasecelo/withdraw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default class Withdraw extends ReleaseGoldBaseCommand {
const remainingUnlockedBalance = await this.releaseGoldWrapper.getRemainingUnlockedBalance()
const maxDistribution = await this.releaseGoldWrapper.getMaxDistribution()
const totalWithdrawn = await this.releaseGoldWrapper.getTotalWithdrawn()
kit.defaultAccount = await this.releaseGoldWrapper.getBeneficiary()
await newCheckBuilder(this)
.addCheck('Value does not exceed available unlocked celo', () =>
value.lte(remainingUnlockedBalance)
Expand All @@ -53,9 +54,9 @@ export default class Withdraw extends ReleaseGoldBaseCommand {
return true
}
)
.isNotSanctioned(kit.defaultAccount as string)
.runChecks()

kit.defaultAccount = await this.releaseGoldWrapper.getBeneficiary()
await displaySendTx('withdrawTx', this.releaseGoldWrapper.withdraw(value))
}
}
85 changes: 85 additions & 0 deletions packages/cli/src/commands/transfer/celo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { COMPLIANT_ERROR_RESPONSE, SANCTIONED_ADDRESSES } from '@celo/compliance'
import { ContractKit, newKitFromWeb3 } from '@celo/contractkit'
import { testWithGanache } from '@celo/dev-utils/lib/ganache-test'
import Web3 from 'web3'
import { testLocally } from '../../test-utils/cliUtils'
import TransferCelo from './celo'

process.env.NO_SYNCCHECK = 'true'

// Lots of commands, sometimes times out
jest.setTimeout(15000)

testWithGanache('transfer:celo cmd', (web3: Web3) => {
let accounts: string[] = []
let kit: ContractKit

beforeEach(async () => {
kit = newKitFromWeb3(web3)
accounts = await web3.eth.getAccounts()
})

test('can transfer celo', async () => {
const balanceBefore = await kit.getTotalBalance(accounts[0])
const receiverBalanceBefore = await kit.getTotalBalance(accounts[1])
const amountToTransfer = '500000000000000000000'
// Send cUSD to RG contract
await testLocally(TransferCelo, [
'--from',
accounts[0],
'--to',
accounts[1],
'--value',
amountToTransfer,
'--gasCurrency',
'cusd',
])
// RG cUSD balance should match the amount sent
const receiverBalance = await kit.getTotalBalance(accounts[1])
expect(receiverBalance.CELO!.toFixed()).toEqual(
receiverBalanceBefore.CELO!.plus(amountToTransfer).toFixed()
)
// Attempt to send cUSD back
await testLocally(TransferCelo, [
'--from',
accounts[1],
'--to',
accounts[0],
'--value',
amountToTransfer,
'--gasCurrency',
'cusd',
])
const balanceAfter = await kit.getTotalBalance(accounts[0])
expect(balanceBefore.CELO).toEqual(balanceAfter.CELO)
})

test('should fail if to address is sanctioned', async () => {
const spy = jest.spyOn(console, 'log')
await expect(
testLocally(TransferCelo, [
'--from',
accounts[1],
'--to',
SANCTIONED_ADDRESSES[0],
'--value',
'1',
])
).rejects.toThrow()
expect(spy).toHaveBeenCalledWith(expect.stringContaining(COMPLIANT_ERROR_RESPONSE))
})
test('should fail if from address is sanctioned', async () => {
const spy = jest.spyOn(console, 'log')
await expect(
testLocally(TransferCelo, [
'--from',
SANCTIONED_ADDRESSES[0],
'--to',
accounts[0],
'--value',
'1',
])
).rejects.toThrow()
expect(spy).toHaveBeenCalledWith(expect.stringContaining(COMPLIANT_ERROR_RESPONSE))
})
})
6 changes: 5 additions & 1 deletion packages/cli/src/commands/transfer/celo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export default class TransferCelo extends BaseCommand {
kit.defaultAccount = from
const celoToken = await kit.contracts.getGoldToken()

await newCheckBuilder(this).hasEnoughCelo(from, value).runChecks()
await newCheckBuilder(this)
.isNotSanctioned(from)
.isNotSanctioned(to)
.hasEnoughCelo(from, value)
.runChecks()

await (res.flags.comment
? displaySendTx(
Expand Down
71 changes: 71 additions & 0 deletions packages/cli/src/commands/transfer/dollars.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { COMPLIANT_ERROR_RESPONSE, SANCTIONED_ADDRESSES } from '@celo/compliance'
import { ContractKit, newKitFromWeb3 } from '@celo/contractkit'
import { testWithGanache } from '@celo/dev-utils/lib/ganache-test'
import Web3 from 'web3'
import { testLocally } from '../../test-utils/cliUtils'
import TransferCUSD from './dollars'

process.env.NO_SYNCCHECK = 'true'

// Lots of commands, sometimes times out
jest.setTimeout(15000)

testWithGanache('transfer:dollars cmd', (web3: Web3) => {
let accounts: string[] = []
let kit: ContractKit

beforeEach(async () => {
kit = newKitFromWeb3(web3)
accounts = await web3.eth.getAccounts()
})

test('can transfer cusd', async () => {
const balanceBefore = await kit.getTotalBalance(accounts[0])
const receiverBalanceBefore = await kit.getTotalBalance(accounts[1])
const amountToTransfer = '500000000000000000000'
// Send cUSD to RG contract
await testLocally(TransferCUSD, [
'--from',
accounts[0],
'--to',
accounts[1],
'--value',
amountToTransfer,
'--gasCurrency',
'CELO',
])
// RG cUSD balance should match the amount sent
const receiverBalance = await kit.getTotalBalance(accounts[1])
expect(receiverBalance.cUSD!.toFixed()).toEqual(
receiverBalanceBefore.cUSD!.plus(amountToTransfer).toFixed()
)
// Attempt to send cUSD back
await testLocally(TransferCUSD, [
'--from',
accounts[1],
'--to',
accounts[0],
'--value',
amountToTransfer,
'--gasCurrency',
'CELO',
])
const balanceAfter = await kit.getTotalBalance(accounts[0])
expect(balanceBefore.cUSD).toEqual(balanceAfter.cUSD)
})

test('should fail if to address is sanctioned', async () => {
const spy = jest.spyOn(console, 'log')
await expect(
testLocally(TransferCUSD, [
'--from',
accounts[1],
'--to',
SANCTIONED_ADDRESSES[0],
'--value',
'1',
])
).rejects.toThrow()
expect(spy).toHaveBeenCalledWith(expect.stringContaining(COMPLIANT_ERROR_RESPONSE))
})
})
6 changes: 5 additions & 1 deletion packages/cli/src/commands/transfer/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export default class TransferErc20 extends BaseCommand {
} catch {
failWith('Invalid erc20 address')
}
await newCheckBuilder(this).hasEnoughErc20(from, value, res.flags.erc20Address).runChecks()
await newCheckBuilder(this)
.isNotSanctioned(from)
.isNotSanctioned(to)
.hasEnoughErc20(from, value, res.flags.erc20Address)
.runChecks()

await displaySendTx('transfer', celoToken.transfer(to, value.toFixed()))
}
Expand Down
71 changes: 71 additions & 0 deletions packages/cli/src/commands/transfer/euros.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { COMPLIANT_ERROR_RESPONSE, SANCTIONED_ADDRESSES } from '@celo/compliance'
import { ContractKit, newKitFromWeb3 } from '@celo/contractkit'
import { testWithGanache } from '@celo/dev-utils/lib/ganache-test'
import Web3 from 'web3'
import { testLocally } from '../../test-utils/cliUtils'
import TransferEURO from './euros'

process.env.NO_SYNCCHECK = 'true'

// Lots of commands, sometimes times out
jest.setTimeout(15000)

testWithGanache('transfer:euros cmd', (web3: Web3) => {
let accounts: string[] = []
let kit: ContractKit

beforeEach(async () => {
kit = newKitFromWeb3(web3)
accounts = await web3.eth.getAccounts()
})

test('can transfer ceur', async () => {
const balanceBefore = await kit.getTotalBalance(accounts[0])
const receiverBalanceBefore = await kit.getTotalBalance(accounts[1])
const amountToTransfer = '500000000000000000000'
// Send cEUR to RG contract
await testLocally(TransferEURO, [
'--from',
accounts[0],
'--to',
accounts[1],
'--value',
amountToTransfer,
'--gasCurrency',
'CELO',
])
// RG cEUR balance should match the amount sent
const receiverBalance = await kit.getTotalBalance(accounts[1])
expect(receiverBalance.cEUR!.toFixed()).toEqual(
receiverBalanceBefore.cEUR!.plus(amountToTransfer).toFixed()
)
// Attempt to send cEUR back
await testLocally(TransferEURO, [
'--from',
accounts[1],
'--to',
accounts[0],
'--value',
amountToTransfer,
'--gasCurrency',
'CELO',
])
const balanceAfter = await kit.getTotalBalance(accounts[0])
expect(balanceBefore.cEUR).toEqual(balanceAfter.cEUR)
})

test('should fail if to address is sanctioned', async () => {
const spy = jest.spyOn(console, 'log')
await expect(
testLocally(TransferEURO, [
'--from',
accounts[1],
'--to',
SANCTIONED_ADDRESSES[0],
'--value',
'1',
])
).rejects.toThrow()
expect(spy).toHaveBeenCalledWith(expect.stringContaining(COMPLIANT_ERROR_RESPONSE))
})
})
1 change: 1 addition & 0 deletions packages/cli/src/exchange-stable-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default class ExchangeStableBase extends BaseCommand {
}
await newCheckBuilder(this)
.hasEnoughStable(res.flags.from, sellAmount, this._stableCurrency)
.isNotSanctioned(res.flags.from)
.runChecks()

const [stableToken, celoNativeTokenAddress, { mento, brokerAddress }] = await Promise.all([
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/transfer-stable-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export abstract class TransferStableBase extends BaseCommand {

await newCheckBuilder(this)
.hasEnoughStable(from, value, this._stableCurrency)
.isNotSanctioned(from)
.isNotSanctioned(to)
.addConditionalCheck(
`Account can afford transfer and gas paid in ${this._stableCurrency}`,
kit.connection.defaultFeeCurrency === stableToken.address,
Expand Down
Loading

0 comments on commit ae51ca8

Please sign in to comment.