Skip to content

Commit 67c2d0c

Browse files
authored
Merge pull request #102 from Merit-Systems/shafu/enumerable-set
enumerable set
2 parents 475fb22 + 58d30ab commit 67c2d0c

File tree

11 files changed

+1924
-238
lines changed

11 files changed

+1924
-238
lines changed

libraries/Errors.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ library Errors {
3333
string internal constant NOT_AUTHORIZED_DISTRIBUTOR = "Not Authorized Distributor";
3434
string internal constant REPO_ALREADY_INITIALIZED = "Repo Already Initialized";
3535
string internal constant BATCH_LIMIT_EXCEEDED = "Batch Limit Exceeded";
36+
string internal constant CANNOT_REMOVE_ALL_ADMINS = "Cannot Remove All Admins";
3637
}

src/Escrow.sol

Lines changed: 104 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ contract Escrow is Owned, IEscrow {
2222
uint16 public constant MAX_FEE = 1_000; // 10 %
2323

2424
bytes32 public constant SET_ADMIN_TYPEHASH =
25-
keccak256("SetAdmin(uint repoId,uint accountId,address admin,uint nonce,uint deadline)");
25+
keccak256("SetAdmin(uint repoId,uint accountId,address[] admins,uint nonce,uint deadline)");
2626
bytes32 public constant CLAIM_TYPEHASH =
2727
keccak256("Claim(uint[] distributionIds,address recipient,uint nonce,uint deadline)");
2828

@@ -32,8 +32,8 @@ contract Escrow is Owned, IEscrow {
3232
struct Account {
3333
mapping(address => uint) balance; // token → balance
3434
bool hasDistributions; // whether any distributions have occurred
35-
address admin;
36-
mapping(address => bool) distributors; // distributor → authorized?
35+
EnumerableSet.AddressSet admins; // set of authorized admins
36+
EnumerableSet.AddressSet distributors; // set of authorized distributors
3737
bool exists;
3838
}
3939

@@ -75,12 +75,12 @@ contract Escrow is Owned, IEscrow {
7575
/* -------------------------------------------------------------------------- */
7676
/* STATE VARIABLES */
7777
/* -------------------------------------------------------------------------- */
78-
mapping(uint => mapping(uint => Account)) public accounts; // repoId → accountId → Account
78+
mapping(uint => mapping(uint => Account)) accounts; // repoId → accountId → Account
7979

80-
mapping(uint => Distribution) public distributions; // distributionId → Distribution
81-
mapping(uint => RepoAccount) public distributionToRepo; // distributionId → RepoAccount (for repo distributions)
80+
mapping(uint => Distribution) public distributions; // distributionId → Distribution
81+
mapping(uint => RepoAccount) public distributionToRepo; // distributionId → RepoAccount (for repo distributions)
8282

83-
mapping(address => uint) public recipientNonce; // recipient → nonce
83+
mapping(address => uint) public recipientNonce; // recipient → nonce
8484
uint public ownerNonce;
8585

8686
uint public fee;
@@ -104,7 +104,7 @@ contract Escrow is Owned, IEscrow {
104104
/* MODIFIERS */
105105
/* -------------------------------------------------------------------------- */
106106
modifier onlyRepoAdmin(uint repoId, uint accountId) {
107-
require(msg.sender == accounts[repoId][accountId].admin, Errors.NOT_REPO_ADMIN);
107+
require(accounts[repoId][accountId].admins.contains(msg.sender), Errors.NOT_REPO_ADMIN);
108108
_;
109109
}
110110

@@ -137,19 +137,20 @@ contract Escrow is Owned, IEscrow {
137137
/* INIT REPO ADMIN */
138138
/* -------------------------------------------------------------------------- */
139139
function initRepo(
140-
uint repoId,
141-
uint accountId,
142-
address admin,
143-
uint deadline,
144-
uint8 v,
145-
bytes32 r,
146-
bytes32 s
140+
uint repoId,
141+
uint accountId,
142+
address[] calldata admins,
143+
uint deadline,
144+
uint8 v,
145+
bytes32 r,
146+
bytes32 s
147147
) external {
148148
Account storage account = accounts[repoId][accountId];
149149

150-
require(!account.exists, Errors.REPO_ALREADY_INITIALIZED);
151-
require(admin != address(0), Errors.INVALID_ADDRESS);
152-
require(block.timestamp <= deadline, Errors.SIGNATURE_EXPIRED);
150+
require(!account.exists, Errors.REPO_ALREADY_INITIALIZED);
151+
require(admins.length > 0, Errors.INVALID_AMOUNT);
152+
require(admins.length <= batchLimit, Errors.BATCH_LIMIT_EXCEEDED);
153+
require(block.timestamp <= deadline, Errors.SIGNATURE_EXPIRED);
153154

154155
bytes32 digest = keccak256(
155156
abi.encodePacked(
@@ -159,7 +160,7 @@ contract Escrow is Owned, IEscrow {
159160
SET_ADMIN_TYPEHASH,
160161
repoId,
161162
accountId,
162-
admin,
163+
keccak256(abi.encode(admins)),
163164
ownerNonce,
164165
deadline
165166
))
@@ -169,8 +170,12 @@ contract Escrow is Owned, IEscrow {
169170

170171
ownerNonce++;
171172
account.exists = true;
172-
account.admin = admin;
173-
emit AdminSet(repoId, accountId, address(0), admin);
173+
174+
for (uint i; i < admins.length; ++i) {
175+
require(admins[i] != address(0), Errors.INVALID_ADDRESS);
176+
account.admins.add(admins[i]);
177+
emit AdminSet(repoId, accountId, address(0), admins[i]);
178+
}
174179
}
175180

176181
/* -------------------------------------------------------------------------- */
@@ -209,8 +214,8 @@ contract Escrow is Owned, IEscrow {
209214

210215
Account storage account = accounts[repoId][accountId];
211216

212-
bool isAdmin = msg.sender == account.admin;
213-
bool isDistributor = account.distributors[msg.sender];
217+
bool isAdmin = account.admins.contains(msg.sender);
218+
bool isDistributor = account.distributors.contains(msg.sender);
214219
require(isAdmin || isDistributor, Errors.NOT_AUTHORIZED_DISTRIBUTOR);
215220

216221
distributionIds = new uint[](_distributions.length);
@@ -421,7 +426,7 @@ contract Escrow is Owned, IEscrow {
421426
require(distributionIds.length <= batchLimit, Errors.BATCH_LIMIT_EXCEEDED);
422427

423428
for (uint i; i < distributionIds.length; ++i) {
424-
uint distributionId = distributionIds[i];
429+
uint distributionId = distributionIds[i];
425430
Distribution storage distribution = distributions[distributionId];
426431

427432
require(distribution.exists, Errors.INVALID_DISTRIBUTION_ID);
@@ -482,18 +487,44 @@ contract Escrow is Owned, IEscrow {
482487
/* -------------------------------------------------------------------------- */
483488
/* ONLY REPO ADMIN */
484489
/* -------------------------------------------------------------------------- */
485-
function transferRepoAdmin(uint repoId, uint accountId, address newAdmin)
490+
function addAdmins(uint repoId, uint accountId, address[] calldata admins)
486491
external
487492
onlyRepoAdmin(repoId, accountId)
488493
{
489-
require(newAdmin != address(0), Errors.INVALID_ADDRESS);
494+
require(admins.length > 0, Errors.INVALID_AMOUNT);
495+
require(admins.length <= batchLimit, Errors.BATCH_LIMIT_EXCEEDED);
496+
497+
Account storage account = accounts[repoId][accountId];
498+
for (uint i; i < admins.length; ++i) {
499+
address admin = admins[i];
500+
require(admin != address(0), Errors.INVALID_ADDRESS);
501+
if (account.admins.add(admin)) {
502+
emit AdminSet(repoId, accountId, address(0), admin);
503+
}
504+
}
505+
}
490506

491-
address oldAdmin = accounts[repoId][accountId].admin;
492-
accounts[repoId][accountId].admin = newAdmin;
493-
emit RepoAdminChanged(repoId, oldAdmin, newAdmin);
507+
function removeAdmins(uint repoId, uint accountId, address[] calldata admins)
508+
external
509+
onlyRepoAdmin(repoId, accountId)
510+
{
511+
require(admins.length > 0, Errors.INVALID_AMOUNT);
512+
require(admins.length <= batchLimit, Errors.BATCH_LIMIT_EXCEEDED);
513+
514+
Account storage account = accounts[repoId][accountId];
515+
516+
// Ensure we don't remove all admins
517+
require(account.admins.length() > admins.length, Errors.CANNOT_REMOVE_ALL_ADMINS);
518+
519+
for (uint i; i < admins.length; ++i) {
520+
address admin = admins[i];
521+
if (account.admins.remove(admin)) {
522+
emit RepoAdminChanged(repoId, admin, address(0));
523+
}
524+
}
494525
}
495526

496-
function addDistributor(uint repoId, uint accountId, address[] calldata distributors)
527+
function addDistributors(uint repoId, uint accountId, address[] calldata distributors)
497528
external
498529
onlyRepoAdmin(repoId, accountId)
499530
{
@@ -503,14 +534,14 @@ contract Escrow is Owned, IEscrow {
503534
for (uint i; i < distributors.length; ++i) {
504535
address distributor = distributors[i];
505536
require(distributor != address(0), Errors.INVALID_ADDRESS);
506-
if (!account.distributors[distributor]) {
507-
account.distributors[distributor] = true;
537+
if (!account.distributors.contains(distributor)) {
538+
account.distributors.add(distributor);
508539
emit AddedDistributor(repoId, accountId, distributor);
509540
}
510541
}
511542
}
512543

513-
function removeDistributor(uint repoId, uint accountId, address[] calldata distributors)
544+
function removeDistributors(uint repoId, uint accountId, address[] calldata distributors)
514545
external
515546
onlyRepoAdmin(repoId, accountId)
516547
{
@@ -519,8 +550,7 @@ contract Escrow is Owned, IEscrow {
519550
Account storage account = accounts[repoId][accountId];
520551
for (uint i; i < distributors.length; ++i) {
521552
address distributor = distributors[i];
522-
if (account.distributors[distributor]) {
523-
account.distributors[distributor] = false;
553+
if (account.distributors.remove(distributor)) {
524554
emit RemovedDistributor(repoId, accountId, distributor);
525555
}
526556
}
@@ -547,28 +577,54 @@ contract Escrow is Owned, IEscrow {
547577
/* -------------------------------------------------------------------------- */
548578
/* GETTERS */
549579
/* -------------------------------------------------------------------------- */
550-
function getAccountAdmin(uint repoId, uint accountId)
580+
function getAllAdmins(uint repoId, uint accountId)
551581
external
552582
view
553-
returns (address)
583+
returns (address[] memory admins)
554584
{
555-
return accounts[repoId][accountId].admin;
585+
EnumerableSet.AddressSet storage adminSet = accounts[repoId][accountId].admins;
586+
uint len = adminSet.length();
587+
admins = new address[](len);
588+
for (uint i; i < len; ++i) {
589+
admins[i] = adminSet.at(i);
590+
}
591+
}
592+
593+
function getIsAuthorizedAdmin(uint repoId, uint accountId, address admin)
594+
external
595+
view
596+
returns (bool)
597+
{
598+
return accounts[repoId][accountId].admins.contains(admin);
556599
}
557600

558601
function getIsAuthorizedDistributor(uint repoId, uint accountId, address distributor)
559602
external
560603
view
561604
returns (bool)
562605
{
563-
return accounts[repoId][accountId].distributors[distributor];
606+
return accounts[repoId][accountId].distributors.contains(distributor);
564607
}
565608

566609
function canDistribute(uint repoId, uint accountId, address caller)
567610
external
568611
view
569612
returns (bool)
570613
{
571-
return caller == accounts[repoId][accountId].admin || accounts[repoId][accountId].distributors[caller];
614+
return accounts[repoId][accountId].admins.contains(caller) || accounts[repoId][accountId].distributors.contains(caller);
615+
}
616+
617+
function getAllDistributors(uint repoId, uint accountId)
618+
external
619+
view
620+
returns (address[] memory distributors)
621+
{
622+
EnumerableSet.AddressSet storage distributorSet = accounts[repoId][accountId].distributors;
623+
uint len = distributorSet.length();
624+
distributors = new address[](len);
625+
for (uint i; i < len; ++i) {
626+
distributors[i] = distributorSet.at(i);
627+
}
572628
}
573629

574630
function getAccountBalance(uint repoId, uint accountId, address token)
@@ -587,6 +643,14 @@ contract Escrow is Owned, IEscrow {
587643
return accounts[repoId][accountId].hasDistributions;
588644
}
589645

646+
function getAccountExists(uint repoId, uint accountId)
647+
external
648+
view
649+
returns (bool)
650+
{
651+
return accounts[repoId][accountId].exists;
652+
}
653+
590654
function getDistribution(uint distributionId)
591655
external
592656
view

test/01_Deploy.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ contract Deploy_Test is Base_Test {
249249
assertEq(deployedEscrow.MAX_FEE(), 1000);
250250

251251
// Verify type hashes
252-
bytes32 expectedSetAdminTypehash = keccak256("SetAdmin(uint repoId,uint accountId,address admin,uint nonce,uint deadline)");
252+
bytes32 expectedSetAdminTypehash = keccak256("SetAdmin(uint repoId,uint accountId,address[] admins,uint nonce,uint deadline)");
253253
bytes32 expectedClaimTypehash = keccak256("Claim(uint[] distributionIds,address recipient,uint nonce,uint deadline)");
254254

255255
assertEq(deployedEscrow.SET_ADMIN_TYPEHASH(), expectedSetAdminTypehash);

0 commit comments

Comments
 (0)