Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reserve supports multiple exchanges #6299

Merged
merged 44 commits into from
Jan 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ee65bae
Added exchange spenders to the reserve
martinvol Dec 18, 2020
90dea2d
Added migration
martinvol Dec 18, 2020
e2e8e91
removed comments
martinvol Dec 18, 2020
4aa87f5
Update packages/protocol/contracts/stability/Reserve.sol
martinvol Dec 24, 2020
1dac950
Update packages/protocol/test/stability/reserve.ts
martinvol Dec 24, 2020
065dcbb
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 4, 2021
bca92c8
Merge branch 'martinvol/multipleExchangeSpenders' of github.com:celo-…
martinvol Jan 4, 2021
1bd660c
Added spender to the migration
martinvol Jan 4, 2021
d7ce70e
Changed comment
martinvol Jan 4, 2021
2852d80
Lint
martinvol Jan 4, 2021
0deb24d
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 4, 2021
c3269fd
Completed migration
martinvol Jan 4, 2021
11633d6
Rolled back reserve change
martinvol Jan 4, 2021
db37799
Reserve can only send to exchanges without limit
martinvol Jan 4, 2021
28a0c78
lint
martinvol Jan 4, 2021
b94186a
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 4, 2021
e72df67
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 4, 2021
fdbdc9b
Rolled backk reserve security changes
martinvol Jan 5, 2021
ae301d0
fixed minor test
martinvol Jan 5, 2021
e5eadd7
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 5, 2021
de9472e
Added conventions
martinvol Jan 6, 2021
e96880b
Merge branch 'martinvol/multipleExchangeSpenders' of github.com:celo-…
martinvol Jan 6, 2021
98c85b2
WIP list of exchange spender addresses in progress
martinvol Jan 11, 2021
0235c34
keeps a list of exchange spenders
martinvol Jan 12, 2021
f3943b2
Cleanup
martinvol Jan 12, 2021
dc7c49b
cleaunp
martinvol Jan 13, 2021
108b86e
Checks for ownership in the test
martinvol Jan 13, 2021
5ed889f
Removed struct
martinvol Jan 13, 2021
1bf4209
Changed interface
martinvol Jan 13, 2021
acbc501
Added interface
martinvol Jan 14, 2021
b1a46e9
Added exchange by default
martinvol Jan 14, 2021
03ce0c1
Added 0x0 just to be safe
martinvol Jan 14, 2021
89c6beb
Small capitallization and check
martinvol Jan 14, 2021
0423759
Lint
martinvol Jan 14, 2021
588f0b7
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 14, 2021
5ede531
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 15, 2021
b439e76
Made view public
martinvol Jan 15, 2021
721273a
Rolled back exchange changes
martinvol Jan 16, 2021
dd97748
Merge branch 'martinvol/multipleExchangeSpenders' of github.com:celo-…
martinvol Jan 16, 2021
7da57a2
lint
martinvol Jan 16, 2021
ecb3f3c
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 16, 2021
d163051
lint
martinvol Jan 16, 2021
ffc785b
Merge branch 'martinvol/multipleExchangeSpenders' of github.com:celo-…
martinvol Jan 16, 2021
9dc4018
Merge branch 'master' into martinvol/multipleExchangeSpenders
martinvol Jan 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions packages/protocol/contracts/stability/Reserve.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ contract Reserve is
uint256 public frozenReserveGoldStartDay;
uint256 public frozenReserveGoldDays;

mapping(address => bool) public isExchangeSpender;
address[] public exchangeSpenderAddresses;

event TobinTaxStalenessThresholdSet(uint256 value);
event DailySpendingRatioSet(uint256 ratio);
event TokenAdded(address indexed token);
Expand All @@ -67,6 +70,8 @@ contract Reserve is
event ReserveGoldTransferred(address indexed spender, address indexed to, uint256 value);
event TobinTaxSet(uint256 value);
event TobinTaxReserveRatioSet(uint256 value);
event ExchangeSpenderAdded(address indexed exchangeSpender);
event ExchangeSpenderRemoved(address indexed exchangeSpender);

modifier isStableToken(address token) {
require(isToken[token], "token addr was never registered");
Expand All @@ -78,7 +83,7 @@ contract Reserve is
* @return The storage, major, minor, and patch version of the contract.
*/
function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) {
return (1, 1, 1, 0);
return (1, 1, 2, 0);
}

function() external payable {} // solhint-disable no-empty-blocks
Expand Down Expand Up @@ -302,10 +307,50 @@ contract Reserve is
* @param spender The address that is to be no longer allowed to spend Reserve funds.
*/
function removeSpender(address spender) external onlyOwner {
isSpender[spender] = false;
delete isSpender[spender];
emit SpenderRemoved(spender);
}

function isAllowedToSpendExchange(address spender) public view returns (bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing natspec

return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function necessary? Please include a comment explaining why isExchangeSpender is not enough, and the conditions that need to be met for this function to be deprecated

isExchangeSpender[spender] || (registry.getAddressForOrDie(EXCHANGE_REGISTRY_ID) == spender);
}

/**
* @notice Gives an address permission to spend Reserve without limit.
* @param spender The address that is allowed to spend Reserve funds.
*/
function addExchangeSpender(address spender) external onlyOwner {
martinvol marked this conversation as resolved.
Show resolved Hide resolved
require(!isExchangeSpender[spender], "Address is already Exchange Spender");
isExchangeSpender[spender] = true;
exchangeSpenderAddresses.push(spender);
emit ExchangeSpenderAdded(spender);
}

/**
* @notice Takes away an address's permission to spend Reserve funds without limits.
* @param spender The address that is to be no longer allowed to spend Reserve funds.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing parameter in natspec

*/
function removeExchangeSpender(address spender, uint256 index) external onlyOwner {
isExchangeSpender[spender] = false;
uint256 numAddresses = exchangeSpenderAddresses.length;
require(index < numAddresses, "Index is invalid");
require(spender == exchangeSpenderAddresses[index], "Index does not match spender");
uint256 newNumAddresses = numAddresses.sub(1);

if (index != newNumAddresses) {
exchangeSpenderAddresses[index] = exchangeSpenderAddresses[newNumAddresses];
}

exchangeSpenderAddresses[newNumAddresses] = address(0x0);
exchangeSpenderAddresses.length = newNumAddresses;
emit ExchangeSpenderRemoved(spender);
}

function getExchangeSpenders() public view returns (address[] memory) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this? exchangeSpenderAddresses is a public variable so this is redundant

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is only used for testing-- think it's worth removing? iirc there isn't a clean way of getting exchangeSpenderAddresses.length from web3 to properly iterate through. Because it's already in there for CR3 I don't think we would gain much now from removing it, if anything it would introduce backward incompatibility

return exchangeSpenderAddresses;
}

/**
* @notice Transfer gold to a whitelisted address subject to reserve spending limits.
* @param to The address that will receive the gold.
Expand Down Expand Up @@ -345,11 +390,8 @@ contract Reserve is
* @param value The amount of gold to transfer.
* @return Returns true if the transaction succeeds.
*/
function transferExchangeGold(address payable to, uint256 value)
external
onlyRegisteredContract(EXCHANGE_REGISTRY_ID)
returns (bool)
{
function transferExchangeGold(address payable to, uint256 value) external returns (bool) {
martinvol marked this conversation as resolved.
Show resolved Hide resolved
require(isAllowedToSpendExchange(msg.sender), "Address not allowed to spend");
return _transferGold(to, value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ interface IReserve {
function getOrComputeTobinTax() external returns (uint256, uint256);
function getTokens() external view returns (address[] memory);
function getReserveRatio() external view returns (uint256);
function addExchangeSpender(address) external;
function removeExchangeSpender(address, uint256) external;
function addSpender(address) external;
function removeSpender(address) external;
}
122 changes: 121 additions & 1 deletion packages/protocol/test/stability/reserve.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CeloContractName } from '@celo/protocol/lib/registry-utils'
import {
assertEqualBN,
assertLogMatches2,
assertRevert,
assertSameAddress,
timeTravel,
Expand Down Expand Up @@ -35,6 +36,7 @@ contract('Reserve', (accounts: string[]) => {
const nonOwner: string = accounts[1]
const spender: string = accounts[2]
const exchangeAddress: string = accounts[3]
const exchangeSpenderAddress: string = accounts[4]
const aTobinTaxStalenessThreshold: number = 600
const aTobinTax = toFixed(0.005)
const aTobinTaxReserveRatio = toFixed(2)
Expand All @@ -48,6 +50,7 @@ contract('Reserve', (accounts: string[]) => {
mockSortedOracles = await MockSortedOracles.new()
await registry.setAddressFor(CeloContractName.SortedOracles, mockSortedOracles.address)
await registry.setAddressFor(CeloContractName.Exchange, exchangeAddress)

await reserve.initialize(
registry.address,
aTobinTaxStalenessThreshold,
Expand Down Expand Up @@ -283,6 +286,115 @@ contract('Reserve', (accounts: string[]) => {
})
})

describe('#addExchangeSpender(exchangeAddress)', () => {
martinvol marked this conversation as resolved.
Show resolved Hide resolved
it('only allows owner', async () => {
await assertRevert(reserve.addExchangeSpender(nonOwner, { from: nonOwner }))
})

it('should emit addExchangeSpender event on add', async () => {
const resp = await reserve.addExchangeSpender(exchangeSpenderAddress)
const log = resp.logs[0]
assert.equal(resp.logs.length, 1)
assertLogMatches2(log, {
event: 'ExchangeSpenderAdded',
args: {
exchangeSpender: exchangeSpenderAddress,
},
})
})

it('has the right list of exchange spenders after addition', async () => {
martinvol marked this conversation as resolved.
Show resolved Hide resolved
await reserve.addExchangeSpender(exchangeAddress)
await reserve.addExchangeSpender(accounts[1])
const spenders = await reserve.getExchangeSpenders()
assert.equal(spenders.length, 2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to check lists with the same contents in a single assert statement.

assert.equal(spenders[0], exchangeAddress)
assert.equal(spenders[1], accounts[1])
})
})
martinvol marked this conversation as resolved.
Show resolved Hide resolved

describe('#removeExchangeSpender(exchangeAddress)', () => {
beforeEach(async () => {
await reserve.addExchangeSpender(exchangeSpenderAddress)
})

it('only allows owner', async () => {
await assertRevert(reserve.removeExchangeSpender(nonOwner, 0, { from: nonOwner }))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the first argument be exchangeSpenderAddress?

})

it('should emit removeExchangeSpender event on remove', async () => {
const resp = await reserve.removeExchangeSpender(exchangeSpenderAddress, 0)
const log = resp.logs[0]
assert.equal(resp.logs.length, 1)
assertLogMatches2(log, {
event: 'ExchangeSpenderRemoved',
args: {
exchangeSpender: exchangeSpenderAddress,
},
})
})

it('has the right list of exchange spenders', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

const spenders = await reserve.getExchangeSpenders()
assert.equal(spenders.length, 1)
assert.equal(spenders[0], exchangeSpenderAddress)
})

it('has the right list of exchange after removing one', async () => {
await reserve.removeExchangeSpender(exchangeSpenderAddress, 0)
const spenders = await reserve.getExchangeSpenders()
assert.equal(spenders.length, 0)
})

it("can't be removed twice", async () => {
await reserve.removeExchangeSpender(exchangeSpenderAddress, 0)
await assertRevert(reserve.removeExchangeSpender(exchangeSpenderAddress, 0))
})

it("can't delete an index out of range", async () => {
await assertRevert(reserve.removeExchangeSpender(exchangeSpenderAddress, 1))
})

it('removes from a big array', async () => {
await reserve.addExchangeSpender(accounts[1])
await reserve.removeExchangeSpender(exchangeSpenderAddress, 0)
const spenders = await reserve.getExchangeSpenders()
assert.equal(spenders.length, 1)
assert.equal(spenders[0], accounts[1])
})

it("doesn't remove an address with the wrong index", async () => {
await reserve.addExchangeSpender(accounts[1])
await assertRevert(reserve.removeExchangeSpender(exchangeSpenderAddress, 1))
})
})
martinvol marked this conversation as resolved.
Show resolved Hide resolved

describe('#addSpender(spender)', () => {
it('emits on add', async () => {
const addSpenderTx = await reserve.addSpender(spender)

const addExchangeSpenderTxLogs = addSpenderTx.logs.filter((x) => x.event === 'SpenderAdded')
assert(addExchangeSpenderTxLogs.length === 1, 'Did not receive event')
})

it('only allows owner', async () => {
await assertRevert(reserve.addSpender(nonOwner, { from: nonOwner }))
})
})

describe('#removeSpender(spender)', () => {
it('emits on remove', async () => {
const addSpenderTx = await reserve.removeSpender(spender)

const addExchangeSpenderTxLogs = addSpenderTx.logs.filter((x) => x.event === 'SpenderRemoved')
assert(addExchangeSpenderTxLogs.length === 1, 'Did not receive event')
})

it('only allows owner', async () => {
await assertRevert(reserve.removeSpender(nonOwner, { from: nonOwner }))
})
})

describe('#transferExchangeGold()', () => {
const aValue = 10000
let otherReserveAddress: string = ''
Expand All @@ -293,7 +405,7 @@ contract('Reserve', (accounts: string[]) => {
await reserve.addOtherReserveAddress(otherReserveAddress)
})

it('should allow a exchange to call transferExchangeGold', async () => {
it('should allow an exchange to call transferExchangeGold', async () => {
await reserve.transferExchangeGold(nonOwner, aValue, { from: exchangeAddress })
})

Expand All @@ -305,6 +417,14 @@ contract('Reserve', (accounts: string[]) => {
await assertRevert(reserve.transferExchangeGold(nonOwner, aValue, { from: nonOwner }))
})

it('should not allow removed exchange spender addresses to call transferExchangeGold', async () => {
await reserve.addExchangeSpender(exchangeSpenderAddress)
await reserve.removeExchangeSpender(exchangeSpenderAddress, 0)
await assertRevert(
reserve.transferExchangeGold(nonOwner, aValue, { from: exchangeSpenderAddress })
)
})

it('should not allow freezing more gold than is available', async () => {
await assertRevert(reserve.setFrozenGold(aValue + 1, 1))
})
Expand Down