diff --git a/packages/dev-utils/src/migration-override.json b/packages/dev-utils/src/migration-override.json index 1ca5ce37693..66423736933 100644 --- a/packages/dev-utils/src/migration-override.json +++ b/packages/dev-utils/src/migration-override.json @@ -23,7 +23,8 @@ "referendumStageDuration": 100, "executionStageDuration": 100, "minDeposit": 1, - "concurrentProposals": 5 + "concurrentProposals": 5, + "skipTransferOwnership": false }, "governanceApproverMultiSig": { "signatories": [ @@ -31,13 +32,17 @@ ], "numRequiredConfirmations": 1 }, + "grandaMento": { + "approver": "0x5409ED021D9299bf6814279A6A1411A7e866A631", + "spread": 0.01, + "vetoPeriodSeconds": 10800 + }, "oracles": { "reportExpiry": 300 }, "reserve": { "initialBalance": 100000000, "otherAddresses": ["0x91c987bf62D25945dB517BDAa840A6c661374402"] - }, "reserveSpenderMultiSig": { "signatories": ["0x5409ed021d9299bf6814279a6a1411a7e866a631", "0x4404ac8bd8F9618D27Ad2f1485AA1B2cFD82482D"], diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index 9ceb32f940b..32053e17cba 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -25,6 +25,7 @@ export const ProxyContracts = [ 'GoldTokenProxy', 'GovernanceApproverMultiSigProxy', 'GovernanceProxy', + 'GrandaMentoProxy', 'LockedGoldProxy', 'MetaTransactionWalletProxy', 'MetaTransactionWalletDeployerProxy', @@ -74,6 +75,9 @@ export const CoreContracts = [ 'StableToken', 'StableTokenEUR', 'SortedOracles', + + // liquidity + 'GrandaMento', ] const OtherContracts = [ diff --git a/packages/sdk/contractkit/src/base.ts b/packages/sdk/contractkit/src/base.ts index ed079912dae..36db4337a6f 100644 --- a/packages/sdk/contractkit/src/base.ts +++ b/packages/sdk/contractkit/src/base.ts @@ -15,6 +15,7 @@ export enum CeloContract { GasPriceMinimum = 'GasPriceMinimum', GoldToken = 'GoldToken', Governance = 'Governance', + GrandaMento = 'GrandaMento', LockedGold = 'LockedGold', MetaTransactionWallet = 'MetaTransactionWallet', MetaTransactionWalletDeployer = 'MetaTransactionWalletDeployer', diff --git a/packages/sdk/contractkit/src/contract-cache.ts b/packages/sdk/contractkit/src/contract-cache.ts index 7bb5e2d9b18..769aae08bb0 100644 --- a/packages/sdk/contractkit/src/contract-cache.ts +++ b/packages/sdk/contractkit/src/contract-cache.ts @@ -16,6 +16,7 @@ import { FreezerWrapper } from './wrappers/Freezer' import { GasPriceMinimumWrapper } from './wrappers/GasPriceMinimum' import { GoldTokenWrapper } from './wrappers/GoldTokenWrapper' import { GovernanceWrapper } from './wrappers/Governance' +import { GrandaMentoWrapper } from './wrappers/GrandaMento' import { LockedGoldWrapper } from './wrappers/LockedGold' import { MetaTransactionWalletWrapper } from './wrappers/MetaTransactionWallet' import { MetaTransactionWalletDeployerWrapper } from './wrappers/MetaTransactionWalletDeployer' @@ -42,6 +43,7 @@ const WrapperFactories = { [CeloContract.GasPriceMinimum]: GasPriceMinimumWrapper, [CeloContract.GoldToken]: GoldTokenWrapper, [CeloContract.Governance]: GovernanceWrapper, + [CeloContract.GrandaMento]: GrandaMentoWrapper, [CeloContract.LockedGold]: LockedGoldWrapper, // [CeloContract.Random]: RandomWrapper, // [CeloContract.Registry]: RegistryWrapper, @@ -75,6 +77,7 @@ interface WrapperCacheMap { [CeloContract.GasPriceMinimum]?: GasPriceMinimumWrapper [CeloContract.GoldToken]?: GoldTokenWrapper [CeloContract.Governance]?: GovernanceWrapper + [CeloContract.GrandaMento]?: GrandaMentoWrapper [CeloContract.LockedGold]?: LockedGoldWrapper [CeloContract.MetaTransactionWallet]?: MetaTransactionWalletWrapper [CeloContract.MetaTransactionWalletDeployer]?: MetaTransactionWalletDeployerWrapper @@ -143,6 +146,9 @@ export class WrapperCache { getGovernance() { return this.getContract(CeloContract.Governance) } + getGrandaMento() { + return this.getContract(CeloContract.GrandaMento) + } getLockedGold() { return this.getContract(CeloContract.LockedGold) } diff --git a/packages/sdk/contractkit/src/kit.ts b/packages/sdk/contractkit/src/kit.ts index 0decae712b6..5a84764a708 100644 --- a/packages/sdk/contractkit/src/kit.ts +++ b/packages/sdk/contractkit/src/kit.ts @@ -130,6 +130,7 @@ export class ContractKit { this.contracts.getValidators(), this.contracts.getDowntimeSlasher(), this.contracts.getBlockchainParameters(), + this.contracts.getGrandaMento(), ] const contracts = await Promise.all(promises) const res = await Promise.all([ @@ -145,6 +146,7 @@ export class ContractKit { contracts[7].getConfig(), contracts[8].getConfig(), contracts[9].getConfig(), + contracts[10].getConfig(), ]) return { exchanges: res[0], @@ -175,6 +177,7 @@ export class ContractKit { this.contracts.getValidators(), this.contracts.getDowntimeSlasher(), this.contracts.getBlockchainParameters(), + this.contracts.getGrandaMento(), ] const contracts = await Promise.all(promises) const res = await Promise.all([ @@ -190,6 +193,7 @@ export class ContractKit { contracts[7].getHumanReadableConfig(), contracts[8].getHumanReadableConfig(), contracts[9].getConfig(), + contracts[10].getConfig(), ]) return { exchanges: res[0], diff --git a/packages/sdk/contractkit/src/test-utils/transferownership.ts b/packages/sdk/contractkit/src/test-utils/transferownership.ts new file mode 100644 index 00000000000..53787bbbe7d --- /dev/null +++ b/packages/sdk/contractkit/src/test-utils/transferownership.ts @@ -0,0 +1,62 @@ +import { NetworkConfig, timeTravel } from '@celo/dev-utils/lib/ganache-test' +import Web3 from 'web3' +import { newKitFromWeb3 } from '../kit' +import { AccountsWrapper } from '../wrappers/Accounts' +import { Proposal, ProposalTransaction } from '../wrappers/Governance' + +// Implements a transfer ownership function using only contractkit primitives + +const expConfigGovernance = NetworkConfig.governance + +export async function assumeOwnership(web3: Web3, to: string, proposalId: number = 1) { + const kit = newKitFromWeb3(web3) + const ONE_CGLD = web3.utils.toWei('1', 'ether') + const accounts = await web3.eth.getAccounts() + let accountWrapper: AccountsWrapper + accountWrapper = await kit.contracts.getAccounts() + const lockedGold = await kit.contracts.getLockedGold() + + try { + await accountWrapper.createAccount().sendAndWaitForReceipt({ from: accounts[0] }) + await lockedGold.lock().sendAndWaitForReceipt({ from: accounts[0], value: ONE_CGLD }) + } catch (error) { + console.log('Account already created') + } + + const grandaMento = await kit._web3Contracts.getGrandaMento() + const governance = await kit.contracts.getGovernance() + const multiSig = await kit.contracts.getMultiSig(await governance.getApprover()) + + const tenMillionCELO = web3.utils.toWei('10000000') + + await lockedGold.lock().sendAndWaitForReceipt({ value: tenMillionCELO }) + + const ownershiptx: ProposalTransaction = { + value: '0', + to: (grandaMento as any)._address, + input: grandaMento.methods.transferOwnership(to).encodeABI(), + } + const proposal: Proposal = [ownershiptx] + + await governance.propose(proposal, 'URL').sendAndWaitForReceipt({ + from: accounts[0], + value: (await governance.getConfig()).minDeposit.toNumber(), + }) + + const tx = await governance.upvote(proposalId, accounts[1]) + await tx.sendAndWaitForReceipt() + await timeTravel(expConfigGovernance.dequeueFrequency, web3) + await governance.dequeueProposalsIfReady().sendAndWaitForReceipt() + + const tx2 = await governance.approve(proposalId) + const multisigTx = await multiSig.submitOrConfirmTransaction(governance.address, tx2.txo) + await multisigTx.sendAndWaitForReceipt({ from: accounts[0] }) + await timeTravel(expConfigGovernance.approvalStageDuration, web3) + + const tx3 = await governance.vote(proposalId, 'Yes') + await tx3.sendAndWaitForReceipt({ from: accounts[0] }) + await timeTravel(expConfigGovernance.referendumStageDuration, web3) + + const tx4 = await governance.execute(proposalId) + await tx4.sendAndWaitForReceipt() +} diff --git a/packages/sdk/contractkit/src/web3-contract-cache.ts b/packages/sdk/contractkit/src/web3-contract-cache.ts index a120d9d7aea..7bc2bbce812 100644 --- a/packages/sdk/contractkit/src/web3-contract-cache.ts +++ b/packages/sdk/contractkit/src/web3-contract-cache.ts @@ -16,6 +16,7 @@ import { newFreezer } from './generated/Freezer' import { newGasPriceMinimum } from './generated/GasPriceMinimum' import { newGoldToken } from './generated/GoldToken' import { newGovernance } from './generated/Governance' +import { newGrandaMento } from './generated/GrandaMento' import { newIerc20 } from './generated/IERC20' import { newLockedGold } from './generated/LockedGold' import { newMetaTransactionWallet } from './generated/MetaTransactionWallet' @@ -50,6 +51,7 @@ export const ContractFactories = { [CeloContract.GasPriceMinimum]: newGasPriceMinimum, [CeloContract.GoldToken]: newGoldToken, [CeloContract.Governance]: newGovernance, + [CeloContract.GrandaMento]: newGrandaMento, [CeloContract.LockedGold]: newLockedGold, [CeloContract.MetaTransactionWallet]: newMetaTransactionWallet, [CeloContract.MetaTransactionWalletDeployer]: newMetaTransactionWalletDeployer, @@ -124,6 +126,9 @@ export class Web3ContractCache { getGovernance() { return this.getContract(CeloContract.Governance) } + getGrandaMento() { + return this.getContract(CeloContract.GrandaMento) + } getLockedGold() { return this.getContract(CeloContract.LockedGold) } diff --git a/packages/sdk/contractkit/src/wrappers/GrandaMento.test.ts b/packages/sdk/contractkit/src/wrappers/GrandaMento.test.ts new file mode 100644 index 00000000000..98c5ebd95a5 --- /dev/null +++ b/packages/sdk/contractkit/src/wrappers/GrandaMento.test.ts @@ -0,0 +1,160 @@ +import { Address } from '@celo/base/lib/address' +import { NetworkConfig, testWithGanache, timeTravel } from '@celo/dev-utils/lib/ganache-test' +import BigNumber from 'bignumber.js' +import Web3 from 'web3' +import { StableToken } from '../celo-tokens' +import { newKitFromWeb3 } from '../kit' +import { assumeOwnership } from '../test-utils/transferownership' +import { GoldTokenWrapper } from './GoldTokenWrapper' +import { ExchangeProposalState, GrandaMentoWrapper } from './GrandaMento' +import { StableTokenWrapper } from './StableTokenWrapper' + +const expConfig = NetworkConfig.grandaMento + +testWithGanache('GrandaMento Wrapper', (web3: Web3) => { + const kit = newKitFromWeb3(web3) + let accounts: Address[] = [] + let grandaMento: GrandaMentoWrapper + let celoToken: GoldTokenWrapper + let stableToken: StableTokenWrapper + const newLimitMin = new BigNumber('1000') + const newLimitMax = new BigNumber('1000000000000') + let sellAmount: BigNumber + + beforeAll(async () => { + accounts = await web3.eth.getAccounts() + kit.defaultAccount = accounts[0] + grandaMento = await kit.contracts.getGrandaMento() + + stableToken = await kit.contracts.getStableToken(StableToken.cUSD) + celoToken = await kit.contracts.getGoldToken() + }) + + const increaseLimits = async () => { + await ( + await grandaMento.setStableTokenExchangeLimits( + 'StableToken', + newLimitMin.toString(), + newLimitMax.toString() + ) + ).sendAndWaitForReceipt() + } + + describe('No limits sets', () => { + it('gets the proposals', async () => { + const activeProposals = await grandaMento.getActiveProposalIds() + expect(activeProposals).toEqual([]) + }) + + it('fetches empty limits', async () => { + const limits = await grandaMento.stableTokenExchangeLimits(StableToken.cUSD) + expect(limits.minExchangeAmount).toEqBigNumber(new BigNumber(0)) + expect(limits.maxExchangeAmount).toEqBigNumber(new BigNumber(0)) + }) + }) + + it("fetchs a proposal it doesn't exist", async () => { + await expect(grandaMento.getExchangeProposal(0)).rejects.toThrow("Proposal doesn't exist") + }) + + describe('When Granda Mento is enabled', () => { + beforeEach(async () => { + await assumeOwnership(web3, accounts[0]) + await increaseLimits() + }) + + it('updated the config', async () => { + const config = await grandaMento.getConfig() + expect( + config.exchangeLimits.get(kit.celoTokens.getContract(StableToken.cUSD))?.minExchangeAmount + ).toEqBigNumber(new BigNumber(newLimitMin)) + expect( + config.exchangeLimits.get(kit.celoTokens.getContract(StableToken.cUSD))?.maxExchangeAmount + ).toEqBigNumber(new BigNumber(newLimitMax)) + expect( + config.exchangeLimits.get(kit.celoTokens.getContract(StableToken.cEUR))?.minExchangeAmount + ).toEqBigNumber(new BigNumber(0)) + expect( + config.exchangeLimits.get(kit.celoTokens.getContract(StableToken.cEUR))?.maxExchangeAmount + ).toEqBigNumber(new BigNumber(0)) + }) + + it('has new limits', async () => { + const limits = await grandaMento.stableTokenExchangeLimits(StableToken.cUSD) + expect(limits.minExchangeAmount).toEqBigNumber(newLimitMin) + expect(limits.maxExchangeAmount).toEqBigNumber(newLimitMax) + }) + + describe('Has a proposal', () => { + beforeEach(async () => { + sellAmount = new BigNumber('100000000') + await ( + await celoToken.increaseAllowance(grandaMento.address, sellAmount) + ).sendAndWaitForReceipt() + + await ( + await grandaMento.createExchangeProposal( + kit.celoTokens.getContract(StableToken.cUSD), + sellAmount, + true + ) + ).sendAndWaitForReceipt() + }) + + it('executes', async () => { + const activeProposals = await grandaMento.getActiveProposalIds() + expect(activeProposals).not.toEqual([]) + + let proposal = await grandaMento.getExchangeProposal(activeProposals[0]) + expect(proposal.exchanger).toEqual(accounts[0]) + expect(proposal.stableToken).toEqual(stableToken.address) + expect(proposal.sellAmount).toEqBigNumber(sellAmount) + expect(proposal.buyAmount).toEqBigNumber(new BigNumber('99000000')) + expect(proposal.approvalTimestamp).toEqual(new BigNumber(0)) + expect(proposal.state).toEqual(ExchangeProposalState.Proposed) + expect(proposal.sellCelo).toEqual(true) + + await ( + await grandaMento.approveExchangeProposal(activeProposals[0]) + ).sendAndWaitForReceipt() + + proposal = await grandaMento.getExchangeProposal(activeProposals[0]) + + expect(proposal.state).toEqual(ExchangeProposalState.Approved) + await timeTravel(expConfig.vetoPeriodSeconds, web3) + await ( + await grandaMento.executeExchangeProposal(activeProposals[0]) + ).sendAndWaitForReceipt() + + proposal = await grandaMento.getExchangeProposal(activeProposals[0]) + expect(proposal.state).toEqual(ExchangeProposalState.Executed) + }) + + it('cancels proposal', async () => { + await (await grandaMento.cancelExchangeProposal(1)).sendAndWaitForReceipt() + + const proposal = await grandaMento.getExchangeProposal('1') + expect(proposal.state).toEqual(ExchangeProposalState.Cancelled) + }) + }) + }) + + it('#getConfig', async () => { + const config = await grandaMento.getConfig() + // expect(config.approver).toBe(expConfig.approver) // TODO FIX this tests, for some reason `expConfig.approver` is 0x0000...0 even it's writen on the migrations-override.json + expect(config.spread).toEqBigNumber(expConfig.spread) + expect(config.vetoPeriodSeconds).toEqBigNumber(expConfig.vetoPeriodSeconds) + expect( + config.exchangeLimits.get(kit.celoTokens.getContract(StableToken.cUSD))?.minExchangeAmount + ).toEqBigNumber(new BigNumber(0)) + expect( + config.exchangeLimits.get(kit.celoTokens.getContract(StableToken.cUSD))?.maxExchangeAmount + ).toEqBigNumber(new BigNumber(0)) + expect( + config.exchangeLimits.get(kit.celoTokens.getContract(StableToken.cEUR))?.minExchangeAmount + ).toEqBigNumber(new BigNumber(0)) + expect( + config.exchangeLimits.get(kit.celoTokens.getContract(StableToken.cEUR))?.maxExchangeAmount + ).toEqBigNumber(new BigNumber(0)) + }) +}) diff --git a/packages/sdk/contractkit/src/wrappers/GrandaMento.ts b/packages/sdk/contractkit/src/wrappers/GrandaMento.ts new file mode 100644 index 00000000000..1805e323f3e --- /dev/null +++ b/packages/sdk/contractkit/src/wrappers/GrandaMento.ts @@ -0,0 +1,142 @@ +import BigNumber from 'bignumber.js' +import { StableTokenContract } from '../base' +import { StableToken } from '../celo-tokens' +import { GrandaMento } from '../generated/GrandaMento' +import { + BaseWrapper, + fixidityValueToBigNumber, + proxyCall, + proxySend, + valueToBigNumber, +} from './BaseWrapper' + +export enum ExchangeProposalState { + None, + Proposed, + Approved, + Executed, + Cancelled, +} + +export interface GrandaMentoConfig { + approver: string + spread: BigNumber + vetoPeriodSeconds: BigNumber + exchangeLimits: AllStableConfig +} + +export interface StableTokenExchangeLimits { + minExchangeAmount: BigNumber + maxExchangeAmount: BigNumber +} + +export interface ExchangeProposal { + exchanger: string + stableToken: string + sellAmount: BigNumber + buyAmount: BigNumber + approvalTimestamp: BigNumber + state: ExchangeProposalState + sellCelo: boolean +} + +type AllStableConfig = Map + +export class GrandaMentoWrapper extends BaseWrapper { + approver = proxyCall(this.contract.methods.approver) + + spread = proxyCall(this.contract.methods.spread, undefined, fixidityValueToBigNumber) + + vetoPeriodSeconds = proxyCall( + this.contract.methods.vetoPeriodSeconds, + undefined, + valueToBigNumber + ) + + owner = proxyCall(this.contract.methods.owner) + + getActiveProposalIds = proxyCall(this.contract.methods.getActiveProposalIds) + + setStableTokenExchangeLimits = proxySend( + this.kit, + this.contract.methods.setStableTokenExchangeLimits + ) + + approveExchangeProposal = proxySend(this.kit, this.contract.methods.approveExchangeProposal) + + executeExchangeProposal = proxySend(this.kit, this.contract.methods.executeExchangeProposal) + cancelExchangeProposal = proxySend(this.kit, this.contract.methods.cancelExchangeProposal) + + async createExchangeProposal( + stableTokenRegistryId: StableTokenContract, + sellAmount: BigNumber, + sellCelo: true + ) { + const createExchangeProposalInner = proxySend( + this.kit, + this.contract.methods.createExchangeProposal + ) + return createExchangeProposalInner(stableTokenRegistryId, sellAmount.toNumber(), sellCelo) + } + + async getExchangeProposal(exchangeProposalID: string | number): Promise { + const result = await this.contract.methods.exchangeProposals(exchangeProposalID).call() + const state = parseInt(result.state, 10) + + if (state === ExchangeProposalState.None) { + throw new Error("Proposal doesn't exist") + } + + return { + exchanger: result.exchanger, + stableToken: result.stableToken, + sellAmount: new BigNumber(result.sellAmount), + buyAmount: new BigNumber(result.buyAmount), + approvalTimestamp: new BigNumber(result.approvalTimestamp), + sellCelo: result.sellCelo, + state, + } + } + + async stableTokenExchangeLimits( + stableTokenTymbol: StableToken + ): Promise { + const stableTokenRegistryId = this.kit.celoTokens.getContract(stableTokenTymbol) + const result = await this.contract.methods + .stableTokenExchangeLimits(stableTokenRegistryId.toString()) + .call() + return { + minExchangeAmount: new BigNumber(result.minExchangeAmount), + maxExchangeAmount: new BigNumber(result.maxExchangeAmount), + } + } + + async getAllStableTokenLimits(): Promise { + const out: AllStableConfig = new Map() + + const res = await Promise.all( + Object.values(StableToken).map((key) => this.stableTokenExchangeLimits(key)) + ) + + Object.values(StableToken).map((key, index) => + out.set(this.kit.celoTokens.getContract(key), res[index]) + ) + + return out + } + + async getConfig(): Promise { + const res = await Promise.all([ + this.approver(), + this.spread(), + this.vetoPeriodSeconds(), + this.getAllStableTokenLimits(), + ]) + return { + approver: res[0], + spread: res[1], + vetoPeriodSeconds: res[2], + exchangeLimits: res[3], + } + } +}