Skip to content

Commit

Permalink
Implemented AddressSet
Browse files Browse the repository at this point in the history
  • Loading branch information
theethernaut authored and mjlescano committed Jan 20, 2022
1 parent a2c53f7 commit f7656b3
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 71 deletions.
40 changes: 39 additions & 1 deletion packages/core-contracts/contracts/mocks/utils/SetUtilMock.sol
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.0;

import "../../utils/SetUtil.sol";

contract SetUtilMock {
contract Bytes32SetMock {
using SetUtil for SetUtil.Bytes32Set;

SetUtil.Bytes32Set private _set;
Expand All @@ -28,7 +28,45 @@ contract SetUtilMock {
return _set.valueAt(position);
}

function positionOf(bytes32 value) external view returns (uint) {
return _set.positionOf(value);
}

function values() external view returns (bytes32[] memory) {
return _set.values();
}
}

contract AddressSetMock {
using SetUtil for SetUtil.AddressSet;

SetUtil.AddressSet private _set;

function add(address value) external {
_set.add(value);
}

function remove(address value) external {
_set.remove(value);
}

function contains(address value) external view returns (bool) {
return _set.contains(value);
}

function length() external view returns (uint) {
return _set.length();
}

function valueAt(uint position) external view returns (address) {
return _set.valueAt(position);
}

function positionOf(address value) external view returns (uint) {
return _set.positionOf(value);
}

function values() external view returns (address[] memory) {
return _set.values();
}
}
57 changes: 57 additions & 0 deletions packages/core-contracts/contracts/utils/SetUtil.sol
Expand Up @@ -2,13 +2,62 @@
pragma solidity ^0.8.0;

library SetUtil {
// ----------------------------------------
// Address support
// ----------------------------------------

struct AddressSet {
Bytes32Set raw;
}

function add(AddressSet storage set, address value) internal {
add(set.raw, bytes32(uint256(uint160(value))));
}

function remove(AddressSet storage set, address value) internal {
remove(set.raw, bytes32(uint256(uint160(value))));
}

function contains(AddressSet storage set, address value) internal view returns (bool) {
return contains(set.raw, bytes32(uint256(uint160(value))));
}

function length(AddressSet storage set) internal view returns (uint) {
return length(set.raw);
}

function valueAt(AddressSet storage set, uint position) internal view returns (address) {
return address(uint160(uint256(valueAt(set.raw, position))));
}

function positionOf(AddressSet storage set, address value) internal view returns (uint) {
return positionOf(set.raw, bytes32(uint256(uint160(value))));
}

function values(AddressSet storage set) internal view returns (address[] memory) {
bytes32[] memory store = values(set.raw);
address[] memory result;

assembly {
result := store
}

return result;
}

// ----------------------------------------
// Core bytes32 support
// ----------------------------------------

error PositionOutOfBounds();
error ValueNotInSet();
error CannotAppendExistingValue();

struct Bytes32Set {
/* solhint-disable private-vars-leading-underscore */
bytes32[] _values;
mapping(bytes32 => uint) _positions;
/* solhint-enable private-vars-leading-underscore */
}

function add(Bytes32Set storage set, bytes32 value) internal {
Expand Down Expand Up @@ -59,6 +108,14 @@ library SetUtil {
return set._values[position];
}

function positionOf(Bytes32Set storage set, bytes32 value) internal view returns (uint) {
if (!contains(set, value)) {
revert ValueNotInSet();
}

return set._positions[value];
}

function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
return set._values;
}
Expand Down
184 changes: 114 additions & 70 deletions packages/core-contracts/test/contracts/utils/SetUtil.test.js
Expand Up @@ -3,116 +3,160 @@ const assert = require('assert/strict');
const assertBn = require('@synthetixio/core-js/utils/assertions/assert-bignumber');
const assertRevert = require('@synthetixio/core-js/utils/assertions/assert-revert');

describe.only('Set', () => {
let Bytes32Set;
describe('SetUtil', () => {
const repeater = [1, 2, 3, 4, 5, 6, 7, 8];

const expectedValues = [];
// -----------------------------------------
// Specific type tests
// -----------------------------------------

const SOME_VALUE = '0x000000000000000000000000000000000000000000000000000000000000beef';
describe('Bytes32Set', function () {
const randomBytes32 = () => ethers.Wallet.createRandom().privateKey;

before('deploy the contract', async () => {
const factory = await ethers.getContractFactory('SetUtilMock');
Bytes32Set = await factory.deploy();
itSupportsType(
'Bytes32Set',
repeater.map(() => randomBytes32()),
randomBytes32()
);
});

const itBehavesAsAValidSet = () => {
it('has the expected length', async function () {
assertBn.eq(await Bytes32Set.length(), expectedValues.length);
});
describe('AddressSet', function () {
const randomAddress = () => ethers.Wallet.createRandom().address;

it('has the expected values', async function () {
assert.deepEqual(await Bytes32Set.values(), expectedValues);
});
itSupportsType(
'AddressSet',
repeater.map(() => randomAddress()),
randomAddress()
);
});

it('contains all values', async function () {
for (let value of expectedValues) {
assert.equal(await Bytes32Set.contains(value), true);
}
});
// -----------------------------------------
// Behaviors
// -----------------------------------------

function itSupportsType(typeName, sampleValues, notContainedValue) {
let SampleSet;

const expectedValues = [];

const addValue = async (value) => {
expectedValues.push(value);

await SampleSet.add(value);
};

const removeValue = async (value) => {
const index = expectedValues.indexOf(value);

if (index !== expectedValues.length - 1) {
const lastValue = expectedValues[expectedValues.length - 1];

it('can retrieve values', async function () {
for (let position = 0; position < expectedValues.length; position++) {
assert.equal(await Bytes32Set.valueAt(position), expectedValues[position]);
expectedValues[index] = lastValue;
}
});

it('does not contain a value not in the set', async function () {
assert.equal(await Bytes32Set.contains(SOME_VALUE), false);
});
expectedValues.pop();

it('reverts when trying to access a value not in the set', async function () {
await assertRevert(Bytes32Set.valueAt(1337), 'PositionOutOfBounds');
});
};
await SampleSet.remove(value);
};

const addValue = async (value) => {
expectedValues.push(value);
function itBehavesAsAValidSet() {
it('has the expected length', async function () {
assertBn.eq(await SampleSet.length(), expectedValues.length);
});

await Bytes32Set.add(value);
};
it('has the expected values', async function () {
assert.deepEqual(await SampleSet.values(), expectedValues);
});

const removeValue = async (value) => {
const index = expectedValues.indexOf(value);
it('contains all values', async function () {
for (let value of expectedValues) {
assert.equal(await SampleSet.contains(value), true);
}
});

if (index !== expectedValues.length - 1) {
const lastValue = expectedValues[expectedValues.length - 1];
it('can retrieve values', async function () {
for (let position = 0; position < expectedValues.length; position++) {
assert.equal(await SampleSet.valueAt(position), expectedValues[position]);
}
});

expectedValues[index] = lastValue;
}
it('does not contain a value not in the set', async function () {
assert.equal(await SampleSet.contains(notContainedValue), false);
});

expectedValues.pop();
it('reverts when trying to access a value not in the set', async function () {
await assertRevert(SampleSet.valueAt(1337), 'PositionOutOfBounds');
});

await Bytes32Set.remove(value);
};
it('reverts when trying to get the position of a value not in the set', async function () {
await assertRevert(SampleSet.positionOf(notContainedValue), 'ValueNotInSet');
});

describe('before any values are added to the set', function () {
itBehavesAsAValidSet();
});
it('reverts when trying to remove a value not in the set', async function () {
await assertRevert(SampleSet.remove(notContainedValue), 'ValueNotInSet');
});

it('reverts when trying to append a value already exsiting in set', async function () {
if (expectedValues.length > 0) {
await assertRevert(SampleSet.add(expectedValues[0]), 'CannotAppendExistingValue');
}
});
}

describe('when some values are added to the set', function () {
before('add values', async function () {
await addValue('0x0000000000000000000000000000000000000000000000000000000deadbeef0');
await addValue('0x0000000000000000000000000000000000000000000000000000000deadbeef1');
await addValue('0x0000000000000000000000000000000000000000000000000000000deadbeef2');
await addValue('0x0000000000000000000000000000000000000000000000000000000deadbeef3');
before('deploy the contract', async () => {
const factory = await ethers.getContractFactory(`${typeName}Mock`);
SampleSet = await factory.deploy();
});

itBehavesAsAValidSet();
describe('before any values are added to the set', function () {
itBehavesAsAValidSet();
});

describe('when more values are added to the set', function () {
describe('when some values are added to the set', function () {
before('add values', async function () {
await addValue('0x0000000000000000000000000000000000000000000000000000000deadbeef4');
await addValue('0x0000000000000000000000000000000000000000000000000000000deadbeef5');
await addValue(sampleValues[0]);
await addValue(sampleValues[1]);
await addValue(sampleValues[2]);
await addValue(sampleValues[3]);
});

itBehavesAsAValidSet();

describe('when some values are removed from the set', function () {
before('remove values', async function () {
await removeValue('0x0000000000000000000000000000000000000000000000000000000deadbeef0');
await removeValue('0x0000000000000000000000000000000000000000000000000000000deadbeef1');
});

itBehavesAsAValidSet();
});

describe('when more values are added to the set', function () {
before('add values', async function () {
await addValue('0x0000000000000000000000000000000000000000000000000000000deadbeef6');
await addValue('0x0000000000000000000000000000000000000000000000000000000deadbeef7');
await addValue(sampleValues[4]);
await addValue(sampleValues[5]);
});

itBehavesAsAValidSet();

describe('when some values are removed from the set', function () {
before('remove values', async function () {
await removeValue('0x0000000000000000000000000000000000000000000000000000000deadbeef2');
await removeValue('0x0000000000000000000000000000000000000000000000000000000deadbeef5');
await removeValue(sampleValues[0]);
await removeValue(sampleValues[1]);
});

itBehavesAsAValidSet();
});

describe('when more values are added to the set', function () {
before('add values', async function () {
await addValue(sampleValues[6]);
await addValue(sampleValues[7]);
});

itBehavesAsAValidSet();

describe('when some values are removed from the set', function () {
before('remove values', async function () {
await removeValue(sampleValues[2]);
await removeValue(sampleValues[5]);
});

itBehavesAsAValidSet();
});
});
});
});
});
}
});

0 comments on commit f7656b3

Please sign in to comment.