Skip to content

Commit

Permalink
Implement OdisPayments.sol and unit tests (#9740)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
eelanagaraj committed Aug 30, 2022
1 parent c1bd057 commit 1452ccc
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 7 deletions.
2 changes: 1 addition & 1 deletion packages/cli/package.json
Expand Up @@ -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"
},
Expand Down
75 changes: 75 additions & 0 deletions 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);
}
}
@@ -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);
}
@@ -0,0 +1,6 @@
pragma solidity ^0.5.13;

import "../../common/Proxy.sol";

/* solhint-disable no-empty-blocks */
contract OdisPaymentsProxy is Proxy {}
3 changes: 3 additions & 0 deletions packages/protocol/governanceConstitution.js
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/lib/registry-utils.ts
Expand Up @@ -30,6 +30,7 @@ export enum CeloContractName {
GovernanceApproverMultiSig = 'GovernanceApproverMultiSig',
GrandaMento = 'GrandaMento',
LockedGold = 'LockedGold',
OdisPayments = 'OdisPayments',
Random = 'Random',
Reserve = 'Reserve',
ReserveSpenderMultiSig = 'ReserveSpenderMultiSig',
Expand Down Expand Up @@ -62,6 +63,7 @@ export const hasEntryInRegistry: string[] = [
CeloContractName.GoldToken,
CeloContractName.GovernanceSlasher,
CeloContractName.GrandaMento,
CeloContractName.OdisPayments,
CeloContractName.Random,
CeloContractName.Reserve,
CeloContractName.SortedOracles,
Expand Down
14 changes: 14 additions & 0 deletions 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<OdisPaymentsInstance>(
web3,
artifacts,
CeloContractName.OdisPayments,
initializeArgs
)
Expand Up @@ -92,6 +92,7 @@ module.exports = deploymentForCoreContract<GovernanceInstance>(
'GovernanceSlasher',
'GrandaMento',
'LockedGold',
'OdisPayments',
'Random',
'Registry',
'Reserve',
Expand Down
@@ -1,3 +1,4 @@
{
"FederatedAttestations": []
"FederatedAttestations": [],
"OdisPayments": []
}
5 changes: 3 additions & 2 deletions packages/protocol/scripts/bash/backupmigrations.sh
Expand Up @@ -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
2 changes: 2 additions & 0 deletions packages/protocol/scripts/build.ts
Expand Up @@ -30,6 +30,7 @@ export const ProxyContracts = [
'LockedGoldProxy',
'MetaTransactionWalletProxy',
'MetaTransactionWalletDeployerProxy',
'OdisPaymentsProxy',
'RegistryProxy',
'ReserveProxy',
'ReserveSpenderMultiSigProxy',
Expand Down Expand Up @@ -69,6 +70,7 @@ export const CoreContracts = [
'Escrow',
'FederatedAttestations',
'Random',
'OdisPayments',

// stability
'Exchange',
Expand Down
155 changes: 155 additions & 0 deletions 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)
})
})
})
2 changes: 1 addition & 1 deletion packages/sdk/contractkit/package.json
Expand Up @@ -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 ."
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/identity/package.json
Expand Up @@ -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 .",
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/transactions-uri/package.json
Expand Up @@ -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 .",
Expand Down

0 comments on commit 1452ccc

Please sign in to comment.