Skip to content

Commit

Permalink
cEUR ContractKit support (#7257)
Browse files Browse the repository at this point in the history
### Description

Adds StableTokenEUR and ExchangeEUR wrappers.

* Adds `Erc20Wrapper`, which wraps the required functions of ERC20 tokens found in the `IERC20` interface
* Adds `CeloTokenWrapper`, which extends `Erc20Wrapper` and wraps functions in the `ICeloToken` interface. Although `ICeloToken` itself doesn't extend `IERC20`, it's intended to be used alongside `IERC20`, and having `CeloTokenWrapper` extend `Erc20Wrapper` is easier to get around TypeScript not supporting multiple inheritance (eg `class StableTokenEURWrapper extends CeloTokenWrapper, Erc20Wrapper` isn't possible)
* `StableTokenWrapper` is used for both cUSD and cEUR.
* `ExchangeWrapper` is used for both cUSD and cEUR. Dollar-specific functions still exist but are deprecated and the generic functions are preferred.
* Created the class `CeloTokens`, a helper class for interacting with CELO & stable tokens

### Other changes

* Using a new version of `@celo/typechain-target-web3-v1-celo` to properly generate TS files for interfaces
* Added `stableTokenEUR` to migration overrides for tests to work

I did not update `packages/sdk/base/src/currencies.ts` to include cEUR as only Valora relies upon that, and adding cEUR there caused a bunch of build issues that can only be resolved with code changes to support cEUR in Valora.

### Tested

* Ran tests
* Ran against a testnet with cEUR

### Related issues

- Fixes #6365

### Backwards compatibility

See Other Changes
  • Loading branch information
tkporter authored Mar 4, 2021
1 parent a8b442e commit ad0328e
Show file tree
Hide file tree
Showing 22 changed files with 821 additions and 401 deletions.
170 changes: 82 additions & 88 deletions packages/celotool/src/e2e-tests/transfer_tests.ts

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions packages/dev-utils/src/migration-override.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"frozen": false,
"minimumReports": 1
},
"exchangeEUR": {
"frozen": false,
"minimumReports": 1
},
"goldToken": {
"frozen": false
},
Expand Down Expand Up @@ -63,6 +67,30 @@
],
"frozen": false
},
"stableTokenEUR": {
"goldPrice": 1,
"initialBalances": {
"addresses": [
"0x5409ED021D9299bf6814279A6A1411A7e866A631",
"0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb",
"0xE36Ea790bc9d7AB70C55260C66D52b1eca985f84",
"0xE834EC434DABA538cd1b9Fe1582052B880BD7e63"
],
"values": [
"50000000000000000000000",
"50000000000000000000000",
"50000000000000000000000",
"50000000000000000000000"
]
},
"oracles": [
"0x5409ED021D9299bf6814279A6A1411A7e866A631",
"0xE36Ea790bc9d7AB70C55260C66D52b1eca985f84",
"0x06cEf8E666768cC40Cc78CF93d9611019dDcB628",
"0x7457d5E02197480Db681D3fdF256c7acA21bDc12"
],
"frozen": false
},
"validators": {
"commissionUpdateDelay": 3
}
Expand Down
2 changes: 1 addition & 1 deletion packages/protocol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"devDependencies": {
"@celo/flake-tracker": "0.0.1-dev",
"@celo/typescript": "0.0.1",
"@celo/typechain-target-web3-v1-celo": "0.1.0-beta3",
"@celo/typechain-target-web3-v1-celo": "0.2.0",
"@types/bn.js": "^4.11.0",
"@types/chai": "^4.1.3",
"@types/mathjs": "^4.4.1",
Expand Down
9 changes: 8 additions & 1 deletion packages/protocol/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const ProxyContracts = [
'ElectionProxy',
'EpochRewardsProxy',
'EscrowProxy',
'ExchangeEURProxy',
'ExchangeProxy',
'FeeCurrencyWhitelistProxy',
'GasPriceMinimumProxy',
Expand All @@ -30,6 +31,7 @@ export const ProxyContracts = [
'RegistryProxy',
'ReserveProxy',
'ReserveSpenderMultiSigProxy',
'StableTokenEURProxy',
'StableTokenProxy',
'SortedOraclesProxy',
]
Expand Down Expand Up @@ -66,9 +68,11 @@ export const CoreContracts = [

// stability
'Exchange',
'ExchangeEUR',
'Reserve',
'ReserveSpenderMultiSig',
'StableToken',
'StableTokenEUR',
'SortedOracles',
]

Expand All @@ -79,6 +83,9 @@ const OtherContracts = [
'Initializable',
'UsingRegistry',
]

const Interfaces = ['ICeloToken', 'IERC20']

export const ImplContracts = OtherContracts.concat(ProxyContracts).concat(CoreContracts)

// const TruffleTestContracts = ['Ownable'].concat(OtherContracts).concat(CoreContracts)
Expand Down Expand Up @@ -134,7 +141,7 @@ async function generateFilesForContractKit() {
exec(`rm -rf ${CONTRACTKIT_GEN_DIR}`)
const relativePath = path.relative(ROOT_DIR, CONTRACTKIT_GEN_DIR)

const contractKitContracts = CoreContracts.concat('Proxy')
const contractKitContracts = CoreContracts.concat('Proxy').concat(Interfaces)

const globPattern = `${BUILD_DIR}/contracts/@(${contractKitContracts.join('|')}).json`

Expand Down
13 changes: 12 additions & 1 deletion packages/sdk/contractkit/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum CeloContract {
EpochRewards = 'EpochRewards',
Escrow = 'Escrow',
Exchange = 'Exchange',
ExchangeEUR = 'ExchangeEUR',
FeeCurrencyWhitelist = 'FeeCurrencyWhitelist',
Freezer = 'Freezer',
GasPriceMinimum = 'GasPriceMinimum',
Expand All @@ -22,13 +23,23 @@ export enum CeloContract {
Reserve = 'Reserve',
SortedOracles = 'SortedOracles',
StableToken = 'StableToken',
StableTokenEUR = 'StableTokenEUR',
TransferWhitelist = 'TransferWhitelist',
Validators = 'Validators',
}

export const ProxyContracts = Object.keys(CeloContract).map((c) => `${c}Proxy`)

export type CeloToken = CeloContract.GoldToken | CeloContract.StableToken
export type StableTokenContract = CeloContract.StableToken | CeloContract.StableTokenEUR

export type ExchangeContract = CeloContract.Exchange | CeloContract.ExchangeEUR

export type CeloTokenContract = StableTokenContract | CeloContract.GoldToken
/**
* Deprecated alias for CeloTokenContract.
* @deprecated Use CeloTokenContract instead
*/
export type CeloToken = CeloTokenContract

export const AllContracts = Object.keys(CeloContract) as CeloContract[]
const AuxiliaryContracts = [
Expand Down
52 changes: 52 additions & 0 deletions packages/sdk/contractkit/src/celo-tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Web3 from 'web3'
import { CeloContract } from './base'
import { CeloTokenInfo, CeloTokens, StableToken, Token } from './celo-tokens'
import { ContractKit, newKitFromWeb3 } from './kit'

describe('CeloTokens', () => {
let kit: ContractKit
let celoTokens: CeloTokens

beforeEach(() => {
kit = newKitFromWeb3(new Web3('http://localhost:8545'))
celoTokens = kit.celoTokens
})

describe('forEachCeloToken()', () => {
it('returns an object with a key for each celo token and the value from a provided async fn', async () => {
const result = await celoTokens.forEachCeloToken(async (info: CeloTokenInfo) =>
Promise.resolve(info.symbol)
)
for (const [key, value] of Object.entries(result)) {
expect(key).toEqual(value)
}
})

it('returns an object with a key for each celo token and the value from a provided non-async fn', async () => {
const result = await celoTokens.forEachCeloToken(async (info: CeloTokenInfo) => info.symbol)
for (const [key, value] of Object.entries(result)) {
expect(key).toEqual(value)
}
})
})

describe('isStableToken()', () => {
it('returns true if the token is a stable token', () => {
expect(celoTokens.isStableToken(StableToken.cUSD)).toEqual(true)
})

it('returns false if the token is not a stable token', () => {
expect(celoTokens.isStableToken(Token.CELO)).toEqual(false)
})
})

describe('isStableTokenContract()', () => {
it('returns true if the contract is a stable token contract', () => {
expect(celoTokens.isStableTokenContract(CeloContract.StableToken)).toEqual(true)
})

it('returns false if the contract is not a stable token contract', () => {
expect(celoTokens.isStableTokenContract(CeloContract.Exchange)).toEqual(false)
})
})
})
201 changes: 201 additions & 0 deletions packages/sdk/contractkit/src/celo-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { BigNumber } from 'bignumber.js'
import { CeloContract, CeloTokenContract, ExchangeContract, StableTokenContract } from './base'
import { ContractKit } from './kit'
import { GoldTokenWrapper } from './wrappers/GoldTokenWrapper'
import { StableTokenWrapper } from './wrappers/StableTokenWrapper'

export enum StableToken {
cUSD = 'cUSD',
cEUR = 'cEUR',
}

export enum Token {
CELO = 'CELO',
}

export type CeloTokenType = StableToken | Token

type CeloTokenWrapper = GoldTokenWrapper | StableTokenWrapper

export type EachCeloToken<T> = {
[key in CeloTokenType]: T
}

export interface CeloTokenInfo {
contract: CeloTokenContract
symbol: CeloTokenType
}

export interface StableTokenInfo extends CeloTokenInfo {
contract: StableTokenContract
exchangeContract: ExchangeContract
}

/** Basic info for each stable token */
const stableTokenInfos: {
[key in StableToken]: StableTokenInfo
} = {
[StableToken.cUSD]: {
contract: CeloContract.StableToken,
exchangeContract: CeloContract.Exchange,
symbol: StableToken.cUSD,
},
[StableToken.cEUR]: {
contract: CeloContract.StableTokenEUR,
exchangeContract: CeloContract.ExchangeEUR,
symbol: StableToken.cEUR,
},
}

/** Basic info for each supported celo token, including stable tokens */
const celoTokenInfos: {
[key in CeloTokenType]: CeloTokenInfo
} = {
[Token.CELO]: {
contract: CeloContract.GoldToken,
symbol: Token.CELO,
},
...stableTokenInfos,
}

/**
* A helper class to interact with all Celo tokens, ie CELO and stable tokens
*/
export class CeloTokens {
constructor(readonly kit: ContractKit) {}

/**
* Gets an address's balance for each celo token.
* @param address the address to look up the balances for
* @return a promise resolving to an object containing the address's balance
* for each celo token
*/
balancesOf(address: string): Promise<EachCeloToken<BigNumber>> {
return this.forEachCeloToken(async (info: CeloTokenInfo) => {
const wrapper = await this.kit.contracts.getContract(info.contract)
return wrapper.balanceOf(address)
})
}

/**
* Gets the wrapper for each celo token.
* @return an promise resolving to an object containing the wrapper for each celo token.
*/
getWrappers(): Promise<EachCeloToken<CeloTokenWrapper>> {
return this.forEachCeloToken((info: CeloTokenInfo) =>
this.kit.contracts.getContract(info.contract)
)
}

/**
* Gets the address for each celo token proxy contract.
* @return an promise resolving to an object containing the address for each celo token proxy.
*/
getAddresses(): Promise<EachCeloToken<string>> {
return this.forEachCeloToken((info: CeloTokenInfo) =>
this.kit.registry.addressFor(info.contract)
)
}

/**
* Runs fn for each celo token found in celoTokenInfos, and returns the
* value of each call in an object keyed by the token.
* @param fn the function to be called for each CeloTokenInfo.
* @return an object containing the resolved value the call to fn for each
* celo token.
*/
async forEachCeloToken<T>(
fn: (info: CeloTokenInfo) => T | Promise<T>
): Promise<EachCeloToken<T>> {
const wrapperInfos = await Promise.all(
Object.values(celoTokenInfos).map(async (info: CeloTokenInfo) => {
const fnResult = fn(info)
return {
symbol: info.symbol,
data: fnResult instanceof Promise ? await fnResult : fnResult,
}
})
)
return wrapperInfos.reduce(
(
obj: {
[key in CeloTokenType]?: T
},
wrapperInfo
) => ({
...obj,
[wrapperInfo.symbol]: wrapperInfo.data,
}),
{}
) as EachCeloToken<T>
}

/**
* Gets the wrapper for a given celo token.
* @param token the token to get the appropriate wrapper for
* @return an promise resolving to the wrapper for the token
*/
getWrapper(token: StableToken): Promise<StableTokenWrapper>
getWrapper(token: CeloTokenType): Promise<CeloTokenWrapper> {
return this.kit.contracts.getContract(celoTokenInfos[token].contract)
}

/**
* Gets the contract for the provided token
* @param token the token to get the contract of
* @return The contract for the token
*/
getContract(token: StableToken): StableTokenContract
getContract(token: CeloTokenType): CeloTokenContract {
return celoTokenInfos[token].contract
}

/**
* Gets the exchange contract for the provided stable token
* @param token the stable token to get exchange contract of
* @return The exchange contract for the token
*/
getExchangeContract(token: StableToken) {
return stableTokenInfos[token].exchangeContract
}

/**
* Gets the address of the contract for the provided token.
* @param token the token to get the (proxy) contract address for
* @return A promise resolving to the address of the token's contract
*/
getAddress(token: CeloTokenType) {
return this.kit.registry.addressFor(celoTokenInfos[token].contract)
}

/**
* Gets the address to use as the feeCurrency when paying for gas with the
* provided token.
* @param token the token to get the feeCurrency address for
* @return If not CELO, the address of the token's contract. If CELO, undefined.
*/
getFeeCurrencyAddress(token: CeloTokenType) {
if (token === Token.CELO) {
return undefined
}
return this.getAddress(token)
}

/**
* Returns if the provided token is a StableToken
* @param token the token
* @return if token is a StableToken
*/
isStableToken(token: CeloTokenType) {
// We cast token as StableToken to make typescript happy
return Object.values(StableToken).includes(token as StableToken)
}

isStableTokenContract(contract: CeloContract) {
const allStableTokenContracts = Object.values(StableToken).map(
(token) => stableTokenInfos[token].contract
)
// We cast token as StableTokenContract to make typescript happy
return allStableTokenContracts.includes(contract as StableTokenContract)
}
}
2 changes: 2 additions & 0 deletions packages/sdk/contractkit/src/contract-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { newKitFromWeb3 } from './kit'
const TestedWrappers: ValidWrappers[] = [
CeloContract.GoldToken,
CeloContract.StableToken,
CeloContract.StableTokenEUR,
CeloContract.Exchange,
CeloContract.ExchangeEUR,
CeloContract.Validators,
CeloContract.LockedGold,
]
Expand Down
Loading

0 comments on commit ad0328e

Please sign in to comment.