From 1452cccf55a506454f6fec72d268b78121c027cd Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Tue, 30 Aug 2022 22:09:09 +0200 Subject: [PATCH] Implement OdisPayments.sol and unit tests (#9740) * Implement OdisPayments.sol and unit tests * Update registry varname because of UsingRegistryV2 changes * Add release data * Fix migration override params in package.json * Address PR comments --- packages/cli/package.json | 2 +- .../contracts/identity/OdisPayments.sol | 75 +++++++++ .../identity/interfaces/IOdisPayments.sol | 6 + .../identity/proxies/OdisPaymentsProxy.sol | 6 + packages/protocol/governanceConstitution.js | 3 + packages/protocol/lib/registry-utils.ts | 2 + .../protocol/migrations/26_odispayments.ts | 14 ++ .../{26_governance.ts => 27_governance.ts} | 1 + ...t_validators.ts => 28_elect_validators.ts} | 0 .../initializationData/release8.json | 3 +- .../protocol/scripts/bash/backupmigrations.sh | 5 +- packages/protocol/scripts/build.ts | 2 + .../protocol/test/identity/odispayments.ts | 155 ++++++++++++++++++ packages/sdk/contractkit/package.json | 2 +- packages/sdk/identity/package.json | 2 +- packages/sdk/transactions-uri/package.json | 2 +- 16 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 packages/protocol/contracts/identity/OdisPayments.sol create mode 100644 packages/protocol/contracts/identity/interfaces/IOdisPayments.sol create mode 100644 packages/protocol/contracts/identity/proxies/OdisPaymentsProxy.sol create mode 100644 packages/protocol/migrations/26_odispayments.ts rename packages/protocol/migrations/{26_governance.ts => 27_governance.ts} (99%) rename packages/protocol/migrations/{27_elect_validators.ts => 28_elect_validators.ts} (100%) create mode 100644 packages/protocol/test/identity/odispayments.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index f3bfe057037..7dafde73a0b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,7 +28,7 @@ "generate:shrinkwrap": "npm install --production && npm shrinkwrap", "check:shrinkwrap": "npm install --production && npm shrinkwrap && ./scripts/check_shrinkwrap_dirty.sh", "prepack": "yarn run build && oclif-dev manifest && oclif-dev readme && yarn run check:shrinkwrap", - "test:reset": "yarn --cwd ../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../dev-utils/src/migration-override.json --upto 26 --release_gold_contracts scripts/truffle/releaseGoldExampleConfigs.json", + "test:reset": "yarn --cwd ../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../dev-utils/src/migration-override.json --upto 27 --release_gold_contracts scripts/truffle/releaseGoldExampleConfigs.json", "test:livechain": "yarn --cwd ../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "TZ=UTC jest --runInBand" }, diff --git a/packages/protocol/contracts/identity/OdisPayments.sol b/packages/protocol/contracts/identity/OdisPayments.sol new file mode 100644 index 00000000000..33caf678322 --- /dev/null +++ b/packages/protocol/contracts/identity/OdisPayments.sol @@ -0,0 +1,75 @@ +pragma solidity ^0.5.13; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +import "./interfaces/IOdisPayments.sol"; +import "../common/interfaces/ICeloVersionedContract.sol"; + +import "../common/Initializable.sol"; +import "../common/UsingRegistryV2.sol"; +import "../common/libraries/ReentrancyGuard.sol"; + +/** + * @title Stores balance to be used for ODIS quota calculation. + */ +contract OdisPayments is + IOdisPayments, + ICeloVersionedContract, + ReentrancyGuard, + Ownable, + Initializable, + UsingRegistryV2 +{ + using SafeMath for uint256; + using SafeERC20 for IERC20; + + event PaymentMade(address indexed account, uint256 valueInCUSD); + + // Store amount sent (all time) from account to this contract. + // Values in totalPaidCUSD should only ever be incremented, since ODIS relies + // on all-time paid balance to compute every quota. + mapping(address => uint256) public totalPaidCUSD; + + /** + * @notice Sets initialized == true on implementation contracts. + * @param test Set to true to skip implementation initialization. + */ + constructor(bool test) public Initializable(test) {} + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + */ + function initialize() external initializer { + _transferOwnership(msg.sender); + } + + /** + * @notice Sends cUSD to this contract to pay for ODIS quota (for queries). + * @param account The account whose balance to increment. + * @param value The amount in cUSD to pay. + * @dev Throws if cUSD transfer fails. + */ + function payInCUSD(address account, uint256 value) external nonReentrant { + IERC20(registryContract.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)).safeTransferFrom( + msg.sender, + address(this), + value + ); + totalPaidCUSD[account] = totalPaidCUSD[account].add(value); + emit PaymentMade(account, value); + } +} diff --git a/packages/protocol/contracts/identity/interfaces/IOdisPayments.sol b/packages/protocol/contracts/identity/interfaces/IOdisPayments.sol new file mode 100644 index 00000000000..98b00d2fceb --- /dev/null +++ b/packages/protocol/contracts/identity/interfaces/IOdisPayments.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +interface IOdisPayments { + function payInCUSD(address account, uint256 value) external; + function totalPaidCUSD(address) external view returns (uint256); +} diff --git a/packages/protocol/contracts/identity/proxies/OdisPaymentsProxy.sol b/packages/protocol/contracts/identity/proxies/OdisPaymentsProxy.sol new file mode 100644 index 00000000000..9114e1a5f93 --- /dev/null +++ b/packages/protocol/contracts/identity/proxies/OdisPaymentsProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../../common/Proxy.sol"; + +/* solhint-disable no-empty-blocks */ +contract OdisPaymentsProxy is Proxy {} diff --git a/packages/protocol/governanceConstitution.js b/packages/protocol/governanceConstitution.js index 440afb7878e..4ee588f4e02 100644 --- a/packages/protocol/governanceConstitution.js +++ b/packages/protocol/governanceConstitution.js @@ -133,6 +133,9 @@ const DefaultConstitution = { addSlasher: 0.9, removeSlasher: 0.8, }, + OdisPayments: { + default: 0.6, + }, // Values for all proxied contracts. proxy: { _transferOwnership: 0.9, diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index ba9063d547d..1bf771971a5 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -30,6 +30,7 @@ export enum CeloContractName { GovernanceApproverMultiSig = 'GovernanceApproverMultiSig', GrandaMento = 'GrandaMento', LockedGold = 'LockedGold', + OdisPayments = 'OdisPayments', Random = 'Random', Reserve = 'Reserve', ReserveSpenderMultiSig = 'ReserveSpenderMultiSig', @@ -62,6 +63,7 @@ export const hasEntryInRegistry: string[] = [ CeloContractName.GoldToken, CeloContractName.GovernanceSlasher, CeloContractName.GrandaMento, + CeloContractName.OdisPayments, CeloContractName.Random, CeloContractName.Reserve, CeloContractName.SortedOracles, diff --git a/packages/protocol/migrations/26_odispayments.ts b/packages/protocol/migrations/26_odispayments.ts new file mode 100644 index 00000000000..947b95214d0 --- /dev/null +++ b/packages/protocol/migrations/26_odispayments.ts @@ -0,0 +1,14 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { OdisPaymentsInstance } from 'types' + +const initializeArgs = async () => { + return [] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.OdisPayments, + initializeArgs +) diff --git a/packages/protocol/migrations/26_governance.ts b/packages/protocol/migrations/27_governance.ts similarity index 99% rename from packages/protocol/migrations/26_governance.ts rename to packages/protocol/migrations/27_governance.ts index a95777f27c2..ce4101a74b9 100644 --- a/packages/protocol/migrations/26_governance.ts +++ b/packages/protocol/migrations/27_governance.ts @@ -92,6 +92,7 @@ module.exports = deploymentForCoreContract( 'GovernanceSlasher', 'GrandaMento', 'LockedGold', + 'OdisPayments', 'Random', 'Registry', 'Reserve', diff --git a/packages/protocol/migrations/27_elect_validators.ts b/packages/protocol/migrations/28_elect_validators.ts similarity index 100% rename from packages/protocol/migrations/27_elect_validators.ts rename to packages/protocol/migrations/28_elect_validators.ts diff --git a/packages/protocol/releaseData/initializationData/release8.json b/packages/protocol/releaseData/initializationData/release8.json index 151187906e6..aa3ba082dc3 100644 --- a/packages/protocol/releaseData/initializationData/release8.json +++ b/packages/protocol/releaseData/initializationData/release8.json @@ -1,3 +1,4 @@ { - "FederatedAttestations": [] + "FederatedAttestations": [], + "OdisPayments": [] } diff --git a/packages/protocol/scripts/bash/backupmigrations.sh b/packages/protocol/scripts/bash/backupmigrations.sh index f4d394cb865..1c50e08d53b 100755 --- a/packages/protocol/scripts/bash/backupmigrations.sh +++ b/packages/protocol/scripts/bash/backupmigrations.sh @@ -47,6 +47,7 @@ else # cp migrations.bak/23_governance_approver_multisig.* migrations/ # cp migrations.bak/24_grandamento.* migrations/ # cp migrations.bak/25_federated_attestations.* migrations/ - # cp migrations.bak/26_governance.* migrations/ - # cp migrations.bak/27_elect_validators.* migrations/ + # cp migrations.bak/26_odispayments.* migrations/ + # cp migrations.bak/27_governance.* migrations/ + # cp migrations.bak/28_elect_validators.* migrations/ fi \ No newline at end of file diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index 0a15471c3be..dcf4889452a 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -30,6 +30,7 @@ export const ProxyContracts = [ 'LockedGoldProxy', 'MetaTransactionWalletProxy', 'MetaTransactionWalletDeployerProxy', + 'OdisPaymentsProxy', 'RegistryProxy', 'ReserveProxy', 'ReserveSpenderMultiSigProxy', @@ -69,6 +70,7 @@ export const CoreContracts = [ 'Escrow', 'FederatedAttestations', 'Random', + 'OdisPayments', // stability 'Exchange', diff --git a/packages/protocol/test/identity/odispayments.ts b/packages/protocol/test/identity/odispayments.ts new file mode 100644 index 00000000000..578d05fb2ee --- /dev/null +++ b/packages/protocol/test/identity/odispayments.ts @@ -0,0 +1,155 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { + assertEqualBN, + assertLogMatches2, + assertRevert, + assumeOwnership, +} from '@celo/protocol/lib/test-utils' +import { getDeployedProxiedContract } from '@celo/protocol/lib/web3-utils' +import { fixed1 } from '@celo/utils/src/fixidity' +import { + FreezerContract, + FreezerInstance, + OdisPaymentsContract, + OdisPaymentsInstance, + RegistryInstance, + StableTokenContract, + StableTokenInstance, +} from 'types' + +const Freezer: FreezerContract = artifacts.require('Freezer') +const OdisPayments: OdisPaymentsContract = artifacts.require('OdisPayments') +const StableTokenCUSD: StableTokenContract = artifacts.require('StableToken') + +const SECONDS_IN_A_DAY = 60 * 60 * 24 + +contract('OdisPayments', (accounts: string[]) => { + let freezer: FreezerInstance + let odisPayments: OdisPaymentsInstance + let registry: RegistryInstance + let stableTokenCUSD: StableTokenInstance + + const owner = accounts[0] + const sender = accounts[1] + const startingBalanceCUSD = 1000 + + before(async () => { + // Mocking Registry.sol when using UsingRegistryV2.sol + registry = await getDeployedProxiedContract('Registry', artifacts) + if ((await registry.owner()) !== owner) { + // In CI we need to assume ownership, locally using quicktest we don't + await assumeOwnership(['Registry'], owner) + } + }) + + beforeEach(async () => { + odisPayments = await OdisPayments.new(true, { from: owner }) + await odisPayments.initialize() + + stableTokenCUSD = await StableTokenCUSD.new(true, { from: owner }) + await registry.setAddressFor(CeloContractName.StableToken, stableTokenCUSD.address) + await stableTokenCUSD.initialize( + 'Celo Dollar', + 'cUSD', + 18, + registry.address, + fixed1, + SECONDS_IN_A_DAY, + // Initialize owner and sender with balances + [owner, sender], + [startingBalanceCUSD, startingBalanceCUSD], + 'Exchange' // USD + ) + + // StableToken is freezable so this is necessary for transferFrom calls + freezer = await Freezer.new(true, { from: owner }) + await freezer.initialize() + await registry.setAddressFor(CeloContractName.Freezer, freezer.address) + }) + + describe('#initialize()', () => { + it('should have set the owner', async () => { + const actualOwner: string = await odisPayments.owner() + assert.equal(actualOwner, owner) + }) + + it('should not be callable again', async () => { + await assertRevert(odisPayments.initialize()) + }) + }) + + describe('#payInCUSD', () => { + const checkStateCUSD = async ( + cusdSender: string, + odisPaymentReceiver: string, + totalValueSent: number + ) => { + assertEqualBN( + await stableTokenCUSD.balanceOf(cusdSender), + startingBalanceCUSD - totalValueSent, + 'cusdSender balance' + ) + assertEqualBN( + await stableTokenCUSD.balanceOf(odisPayments.address), + totalValueSent, + 'odisPayments.address balance' + ) + assertEqualBN( + await odisPayments.totalPaidCUSD(odisPaymentReceiver), + totalValueSent, + 'odisPaymentReceiver balance' + ) + } + + const valueApprovedForTransfer = 10 + const receiver = accounts[2] + + beforeEach(async () => { + await stableTokenCUSD.approve(odisPayments.address, valueApprovedForTransfer, { + from: sender, + }) + assertEqualBN(await stableTokenCUSD.balanceOf(sender), startingBalanceCUSD) + }) + + it('should allow sender to make a payment on their behalf', async () => { + await odisPayments.payInCUSD(sender, valueApprovedForTransfer, { from: sender }) + await checkStateCUSD(sender, sender, valueApprovedForTransfer) + }) + + it('should allow sender to make a payment for another account', async () => { + await odisPayments.payInCUSD(receiver, valueApprovedForTransfer, { from: sender }) + await checkStateCUSD(sender, receiver, valueApprovedForTransfer) + }) + + it('should allow sender to make multiple payments to the contract', async () => { + const valueForSecondTransfer = 5 + const valueForFirstTransfer = valueApprovedForTransfer - valueForSecondTransfer + + await odisPayments.payInCUSD(sender, valueForFirstTransfer, { from: sender }) + await checkStateCUSD(sender, sender, valueForFirstTransfer) + + await odisPayments.payInCUSD(sender, valueForSecondTransfer, { from: sender }) + await checkStateCUSD(sender, sender, valueApprovedForTransfer) + }) + + it('should emit the PaymentMade event', async () => { + const receipt = await odisPayments.payInCUSD(receiver, valueApprovedForTransfer, { + from: sender, + }) + assertLogMatches2(receipt.logs[0], { + event: 'PaymentMade', + args: { + account: receiver, + valueInCUSD: valueApprovedForTransfer, + }, + }) + }) + + it('should revert if transfer fails', async () => { + await assertRevert( + odisPayments.payInCUSD(sender, valueApprovedForTransfer + 1, { from: sender }) + ) + assertEqualBN(await odisPayments.totalPaidCUSD(sender), 0) + }) + }) +}) diff --git a/packages/sdk/contractkit/package.json b/packages/sdk/contractkit/package.json index fd6d7777522..c8c0bdf3e64 100644 --- a/packages/sdk/contractkit/package.json +++ b/packages/sdk/contractkit/package.json @@ -22,7 +22,7 @@ "clean:all": "yarn clean && rm -rf src/generated", "prepublishOnly": "yarn build", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 27", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project ." diff --git a/packages/sdk/identity/package.json b/packages/sdk/identity/package.json index 51a6c8bec66..28ed98023f7 100644 --- a/packages/sdk/identity/package.json +++ b/packages/sdk/identity/package.json @@ -18,7 +18,7 @@ "build": "tsc -b .", "clean": "tsc -b . --clean", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 27", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project .", diff --git a/packages/sdk/transactions-uri/package.json b/packages/sdk/transactions-uri/package.json index 8a847936de7..f737b81c32d 100644 --- a/packages/sdk/transactions-uri/package.json +++ b/packages/sdk/transactions-uri/package.json @@ -17,7 +17,7 @@ "build": "tsc -b .", "clean": "tsc -b . --clean", "docs": "typedoc", - "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 26", + "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 27", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", "test": "jest --runInBand", "lint": "tslint -c tslint.json --project .",