Skip to content

Commit

Permalink
Added daily limit for reserve spending (#2303)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrsmkl authored and celo-ci-bot-user committed Dec 27, 2019
1 parent 5d1a40c commit ce8d150
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 16 deletions.
10 changes: 10 additions & 0 deletions packages/protocol/contracts/common/test/MockGoldToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pragma solidity ^0.5.3;
contract MockGoldToken {
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) balances;

function setTotalSupply(uint256 value) external {
totalSupply = value;
Expand All @@ -19,4 +20,13 @@ contract MockGoldToken {
function transferFrom(address, address, uint256) external pure returns (bool) {
return true;
}

function setBalanceOf(address a, uint256 value) external {
balances[a] = value;
}

function balanceOf(address a) external view returns (uint256) {
return balances[a];
}

}
41 changes: 37 additions & 4 deletions packages/protocol/contracts/stability/Reserve.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ contract Reserve is IReserve, Ownable, Initializable, UsingRegistry, ReentrancyG
bytes32[] public assetAllocationSymbols;
uint256[] public assetAllocationWeights;

uint256 public lastSpendingDay;
uint256 public spendingLimit;
FixidityLib.Fraction private spendingRatio;

event TobinTaxStalenessThresholdSet(uint256 value);
event DailySpendingRatioSet(uint256 ratio);
event TokenAdded(address token);
event TokenRemoved(address token, uint256 index);
event SpenderAdded(address spender);
Expand All @@ -58,13 +63,15 @@ contract Reserve is IReserve, Ownable, Initializable, UsingRegistry, ReentrancyG
* @param registryAddress The address of the registry contract.
* @param _tobinTaxStalenessThreshold The initial number of seconds to cache tobin tax value for.
*/
function initialize(address registryAddress, uint256 _tobinTaxStalenessThreshold)
external
initializer
{
function initialize(
address registryAddress,
uint256 _tobinTaxStalenessThreshold,
uint256 _spendingRatio
) external initializer {
_transferOwnership(msg.sender);
setRegistry(registryAddress);
setTobinTaxStalenessThreshold(_tobinTaxStalenessThreshold);
setDailySpendingRatio(_spendingRatio);
}

/**
Expand All @@ -77,6 +84,24 @@ contract Reserve is IReserve, Ownable, Initializable, UsingRegistry, ReentrancyG
emit TobinTaxStalenessThresholdSet(value);
}

/**
* @notice Set the ratio of reserve that is spendable per day.
* @param ratio Spending ratio as unwrapped Fraction.
*/
function setDailySpendingRatio(uint256 ratio) public onlyOwner {
spendingRatio = FixidityLib.wrap(ratio);
require(spendingRatio.lte(FixidityLib.fixed1()), "spending ratio cannot be larger than 1");
emit DailySpendingRatioSet(ratio);
}

/**
* @notice Get daily spending ratio.
* @return Spending ratio as unwrapped Fraction.
*/
function getDailySpendingRatio() public view onlyOwner returns (uint256) {
return spendingRatio.unwrap();
}

/**
* @notice Sets target allocations for Celo Gold and a diversified basket of non-Celo assets.
* @param symbols The symbol of each asset in the Reserve portfolio.
Expand Down Expand Up @@ -204,6 +229,14 @@ contract Reserve is IReserve, Ownable, Initializable, UsingRegistry, ReentrancyG
*/
function transferGold(address to, uint256 value) external returns (bool) {
require(isSpender[msg.sender], "sender not allowed to transfer Reserve funds");
uint256 currentDay = now / 1 days;
if (currentDay > lastSpendingDay) {
uint256 balance = getReserveGoldBalance();
lastSpendingDay = currentDay;
spendingLimit = spendingRatio.multiply(FixidityLib.newFixed(balance)).fromFixed();
}
require(spendingLimit >= value, "Exceeding spending limit");
spendingLimit = spendingLimit.sub(value);
require(getGoldToken().transfer(to, value), "transfer of gold token failed");
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pragma solidity ^0.5.3;

interface IReserve {
function initialize(address, uint256) external;
function initialize(address, uint256, uint256) external;
function setTobinTaxStalenessThreshold(uint256) external;
function addToken(address) external returns (bool);
function removeToken(address, uint256) external returns (bool);
Expand Down
8 changes: 6 additions & 2 deletions packages/protocol/migrations/07_reserve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ import { config } from '@celo/protocol/migrationsConfig'
import { RegistryInstance, ReserveInstance } from 'types'
const truffle = require('@celo/protocol/truffle-config.js')

const initializeArgs = async (): Promise<[string, number]> => {
const initializeArgs = async (): Promise<[string, number, string]> => {
const registry: RegistryInstance = await getDeployedProxiedContract<RegistryInstance>(
'Registry',
artifacts
)
return [registry.address, config.reserve.tobinTaxStalenessThreshold]
return [
registry.address,
config.reserve.tobinTaxStalenessThreshold,
config.reserve.dailySpendingRatio,
]
}

module.exports = deploymentForCoreContract<ReserveInstance>(
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/migrationsConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const DefaultConfig = {
reserve: {
goldBalance: 100000000,
tobinTaxStalenessThreshold: 60 * 60, // 1 hour
dailySpendingRatio: '1000000000000000000000000', // 100%
},
stableToken: {
decimals: 18,
Expand Down
2 changes: 1 addition & 1 deletion packages/protocol/test/governance/epochrewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ contract('EpochRewards', (accounts: string[]) => {
beforeEach(async () => {
reserve = await Reserve.new()
await registry.setAddressFor(CeloContractName.Reserve, reserve.address)
await reserve.initialize(registry.address, 60)
await reserve.initialize(registry.address, 60, toFixed(1))
await mockGoldToken.setTotalSupply(totalSupply)
await web3.eth.sendTransaction({
from: accounts[9],
Expand Down
55 changes: 47 additions & 8 deletions packages/protocol/test/stability/reserve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
assertSameAddress,
timeTravel,
} from '@celo/protocol/lib/test-utils'
import { toFixed } from '@celo/utils/lib/fixidity'
import BigNumber from 'bignumber.js'
import BN = require('bn.js')
import {
Expand Down Expand Up @@ -35,6 +36,7 @@ contract('Reserve', (accounts: string[]) => {
const nonOwner: string = accounts[1]
const spender: string = accounts[2]
const aTobinTaxStalenessThreshold: number = 600
const aDailySpendingRatio: string = '1000000000000000000000000'
const sortedOraclesDenominator = new BigNumber('0x10000000000000000')
beforeEach(async () => {
reserve = await Reserve.new()
Expand All @@ -43,7 +45,7 @@ contract('Reserve', (accounts: string[]) => {
mockGoldToken = await MockGoldToken.new()
await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address)
await registry.setAddressFor(CeloContractName.GoldToken, mockGoldToken.address)
await reserve.initialize(registry.address, aTobinTaxStalenessThreshold)
await reserve.initialize(registry.address, aTobinTaxStalenessThreshold, aDailySpendingRatio)
})

describe('#initialize()', () => {
Expand All @@ -63,7 +65,29 @@ contract('Reserve', (accounts: string[]) => {
})

it('should not be callable again', async () => {
await assertRevert(reserve.initialize(registry.address, aTobinTaxStalenessThreshold))
await assertRevert(
reserve.initialize(registry.address, aTobinTaxStalenessThreshold, aDailySpendingRatio)
)
})
})

describe('#setDailySpendingRatio()', async () => {
it('should allow owner to set the ratio', async () => {
await reserve.setDailySpendingRatio(123)
assert.equal(123, (await reserve.getDailySpendingRatio()).toNumber())
})
it('should emit corresponding event', async () => {
const response = await reserve.setDailySpendingRatio(123)
const events = response.logs
assert.equal(events.length, 1)
assert.equal(events[0].event, 'DailySpendingRatioSet')
assert.equal(events[0].args.ratio.toNumber(), 123)
})
it('should not allow other users to set the ratio', async () => {
await assertRevert(reserve.setDailySpendingRatio(123, { from: nonOwner }))
})
it('should not be allowed to set it larger than 100%', async () => {
await assertRevert(reserve.setDailySpendingRatio(toFixed(1.3)))
})
})

Expand Down Expand Up @@ -152,20 +176,35 @@ contract('Reserve', (accounts: string[]) => {
})

describe('#transferGold()', () => {
const aValue = 10
const aValue = 10000
beforeEach(async () => {
await web3.eth.sendTransaction({
from: accounts[0],
to: reserve.address,
value: aValue,
})
await mockGoldToken.setBalanceOf(reserve.address, aValue)
await web3.eth.sendTransaction({ to: reserve.address, value: aValue, from: accounts[0] })
await reserve.addSpender(spender)
})

it('should allow a spender to call transferGold', async () => {
await reserve.transferGold(nonOwner, aValue, { from: spender })
})

it('should not allow a spender to transfer more than daily ratio', async () => {
await reserve.setDailySpendingRatio(toFixed(0.2))
await assertRevert(reserve.transferGold(nonOwner, aValue / 2, { from: spender }))
})

it('daily spending accumulates', async () => {
await reserve.setDailySpendingRatio(toFixed(0.15))
await reserve.transferGold(nonOwner, aValue * 0.1, { from: spender })
await assertRevert(reserve.transferGold(nonOwner, aValue * 0.1, { from: spender }))
})

it('daily spending limit should be reset after 24 hours', async () => {
await reserve.setDailySpendingRatio(toFixed(0.15))
await reserve.transferGold(nonOwner, aValue * 0.1, { from: spender })
await timeTravel(3600 * 24, web3)
await reserve.transferGold(nonOwner, aValue * 0.1, { from: spender })
})

it('should not allow a removed spender to call transferGold', async () => {
await reserve.removeSpender(spender)
await assertRevert(reserve.transferGold(nonOwner, aValue, { from: spender }))
Expand Down

0 comments on commit ce8d150

Please sign in to comment.