Skip to content

Commit 25625e9

Browse files
committed
feat: add support for percentage restrictions
1 parent 70319a6 commit 25625e9

File tree

8 files changed

+350
-6
lines changed

8 files changed

+350
-6
lines changed

src/entities/SecurityToken/Features.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { ModuleName } from '@polymathnetwork/contract-wrappers';
1+
import { ModuleName, BigNumber } from '@polymathnetwork/contract-wrappers';
22
import { SubModule } from './SubModule';
33
import {
44
EnableGeneralPermissionManager,
55
EnableDividendManagers,
66
EnableGeneralTransferManager,
77
DisableFeature,
8+
EnablePercentageTransferManager,
89
} from '../../procedures';
910
import {
1011
Feature,
@@ -26,12 +27,14 @@ export interface FeatureStatuses {
2627
[Feature.Erc20Dividends]: boolean;
2728
[Feature.EtherDividends]: boolean;
2829
[Feature.ShareholderCountRestrictions]: boolean;
30+
[Feature.PercentageOwnershipRestrictions]: boolean;
2931
}
3032

3133
type EnableOpts =
3234
| EnableErc20DividendsOpts
3335
| EnableEtherDividendsOpts
34-
| EnableShareholderCountRestrictionsOpts;
36+
| EnableShareholderCountRestrictionsOpts
37+
| EnablePercentageOwnershipRestrictionsOpts;
3538

3639
export interface EnableErc20DividendsOpts {
3740
storageWalletAddress: string;
@@ -45,6 +48,11 @@ export interface EnableShareholderCountRestrictionsOpts {
4548
maxHolderCount: number;
4649
}
4750

51+
export interface EnablePercentageOwnershipRestrictionsOpts {
52+
maxHolderPercentage: BigNumber;
53+
allowPrimaryIssuance?: boolean;
54+
}
55+
4856
export interface Enable {
4957
(args: { feature: Feature.Permissions }): Promise<
5058
TransactionQueue<EnableGeneralPermissionManagerProcedureArgs>
@@ -62,6 +70,10 @@ export interface Enable {
6270
args: { feature: Feature.ShareholderCountRestrictions },
6371
opts: EnableShareholderCountRestrictionsOpts
6472
): Promise<TransactionQueue<EnableCountTransferManagerProcedureArgs>>;
73+
(
74+
args: { feature: Feature.PercentageOwnershipRestrictions },
75+
opts: EnablePercentageOwnershipRestrictionsOpts
76+
): Promise<TransactionQueue<EnableDividendManagersProcedureArgs>>;
6577
}
6678

6779
export class Features extends SubModule {
@@ -74,6 +86,7 @@ export class Features extends SubModule {
7486
Feature.Erc20Dividends,
7587
Feature.EtherDividends,
7688
Feature.ShareholderCountRestrictions,
89+
Feature.PercentageOwnershipRestrictions,
7790
];
7891

7992
/**
@@ -108,6 +121,7 @@ export class Features extends SubModule {
108121
erc20DividendsEnabled,
109122
etherDividendsEnabled,
110123
countTransferManagerEnabled,
124+
percentageTransferManagerEnabled,
111125
] = await Promise.all(list.map(feature => this.isEnabled({ feature })));
112126

113127
const result: FeatureStatuses = {
@@ -116,6 +130,7 @@ export class Features extends SubModule {
116130
[Feature.Erc20Dividends]: erc20DividendsEnabled,
117131
[Feature.EtherDividends]: etherDividendsEnabled,
118132
[Feature.ShareholderCountRestrictions]: countTransferManagerEnabled,
133+
[Feature.PercentageOwnershipRestrictions]: percentageTransferManagerEnabled,
119134
};
120135

121136
return result;
@@ -186,6 +201,13 @@ export class Features extends SubModule {
186201
);
187202
break;
188203
}
204+
case Feature.ShareholderCountRestrictions: {
205+
procedure = new EnablePercentageTransferManager(
206+
{ symbol, ...(opts as EnablePercentageOwnershipRestrictionsOpts) },
207+
this.context
208+
);
209+
break;
210+
}
189211
default: {
190212
throw new PolymathError({
191213
code: ErrorCode.FetcherValidationError,
@@ -247,6 +269,9 @@ export class Features extends SubModule {
247269
case Feature.ShareholderCountRestrictions:
248270
moduleName = ModuleName.CountTransferManager;
249271
break;
272+
case Feature.PercentageOwnershipRestrictions:
273+
moduleName = ModuleName.PercentageTransferManager;
274+
break;
250275
default:
251276
throw new PolymathError({
252277
code: ErrorCode.FetcherValidationError,

src/entities/SecurityToken/Restrictions.ts

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,128 @@
1-
import { ModuleName } from '@polymathnetwork/contract-wrappers';
2-
import { ErrorCode } from '../../types';
3-
import { ModifyMaxHolderCount } from '../../procedures';
1+
import {
2+
ModuleName,
3+
BigNumber,
4+
PercentageTransferManagerEvents,
5+
} from '@polymathnetwork/contract-wrappers';
6+
import { keys } from 'lodash';
7+
import { ErrorCode, PercentageWhitelistEntry } from '../../types';
8+
import {
9+
ModifyMaxHolderCount,
10+
ModifyMaxHolderPercentage,
11+
ModifyPercentageWhitelist,
12+
} from '../../procedures';
413
import { SubModule } from './SubModule';
514
import { PolymathError } from '../../PolymathError';
615

716
export class Restrictions extends SubModule {
17+
/**
18+
* Modify the list of addresses which are exempt from percentage restrictions
19+
*
20+
* @param entries.address address to modify
21+
* @param entries.whitelisted whether the address should be exempt or not
22+
*/
23+
public modifyPercentageWhitelist = async (args: { entries: PercentageWhitelistEntry[] }) => {
24+
const procedure = new ModifyPercentageWhitelist(
25+
{
26+
symbol: this.securityToken.symbol,
27+
...args,
28+
},
29+
this.context
30+
);
31+
return procedure.prepare();
32+
};
33+
34+
/**
35+
* Retrieve the list of shareholder addresses that are exempt from percentage ownership restrictions
36+
*/
37+
public getPercentageWhitelist = async () => {
38+
const {
39+
context: { contractWrappers },
40+
securityToken,
41+
} = this;
42+
43+
const { symbol } = securityToken;
44+
45+
const [percentageTransferManagerModule] = await contractWrappers.getAttachedModules(
46+
{ moduleName: ModuleName.PercentageTransferManager, symbol },
47+
{ unarchived: true }
48+
);
49+
50+
if (!percentageTransferManagerModule) {
51+
throw new PolymathError({
52+
code: ErrorCode.FeatureNotEnabled,
53+
message: 'You must enable the PercentageOwnershipRestrictions Feature',
54+
});
55+
}
56+
57+
const logs = await percentageTransferManagerModule.getLogsAsync({
58+
eventName: PercentageTransferManagerEvents.ModifyWhitelist,
59+
});
60+
const whitelist: {
61+
[address: string]: {
62+
whitelisted: boolean;
63+
blockNumber: number;
64+
};
65+
} = {};
66+
67+
logs.forEach(({ args: { _investor, _valid }, blockNumber }) => {
68+
const entry = whitelist[_investor];
69+
70+
if (!entry || !blockNumber || blockNumber >= entry.blockNumber) {
71+
whitelist[_investor] = {
72+
whitelisted: _valid,
73+
blockNumber: blockNumber || -1,
74+
};
75+
}
76+
});
77+
78+
const addresses = keys(whitelist);
79+
80+
return addresses.filter(address => whitelist[address].whitelisted);
81+
};
82+
83+
/**
84+
* Modify the maximum percentage of the total supply that a single shareholder can own at a given time
85+
*
86+
* @param maxHolderPercentage limit to the percentage a shareholder can own (i.e. `new BigNumber(55.75)` for 55.75%)
87+
*/
88+
public modifyMaxHolderPercentage = async (args: { maxHolderPercentage: BigNumber }) => {
89+
const procedure = new ModifyMaxHolderPercentage(
90+
{
91+
symbol: this.securityToken.symbol,
92+
...args,
93+
},
94+
this.context
95+
);
96+
return procedure.prepare();
97+
};
98+
99+
/**
100+
* Retrieve the maximum percentage of the total supply that a single shareholder can own.
101+
* Can be modified with `modifyMaxHolderPercentage`
102+
*/
103+
public getMaxHolderPercentage = async () => {
104+
const {
105+
context: { contractWrappers },
106+
securityToken,
107+
} = this;
108+
109+
const { symbol } = securityToken;
110+
111+
const percentageTransferManagerModule = (await contractWrappers.getAttachedModules(
112+
{ moduleName: ModuleName.PercentageTransferManager, symbol },
113+
{ unarchived: true }
114+
))[0];
115+
116+
if (!percentageTransferManagerModule) {
117+
throw new PolymathError({
118+
code: ErrorCode.FeatureNotEnabled,
119+
message: 'You must enable the PercentageOwnershipRestrictions Feature',
120+
});
121+
}
122+
123+
return percentageTransferManagerModule.maxHolderPercentage();
124+
};
125+
8126
/**
9127
* Modify the maximum amount of shareholders allowed to hold the token at once
10128
*
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ModuleName, TransactionParams, BigNumber } from '@polymathnetwork/contract-wrappers';
2+
import { Procedure } from './Procedure';
3+
import {
4+
ProcedureType,
5+
PolyTransactionTag,
6+
EnablePercentageTransferManagerProcedureArgs,
7+
ErrorCode,
8+
} from '../types';
9+
import { PolymathError } from '../PolymathError';
10+
11+
export class EnablePercentageTransferManager extends Procedure<
12+
EnablePercentageTransferManagerProcedureArgs
13+
> {
14+
public type = ProcedureType.EnablePercentageTransferManager;
15+
16+
public async prepareTransactions() {
17+
const { symbol, maxHolderPercentage, allowPrimaryIssuance = false } = this.args;
18+
const { contractWrappers } = this.context;
19+
20+
let securityToken;
21+
22+
try {
23+
securityToken = await contractWrappers.tokenFactory.getSecurityTokenInstanceFromTicker(
24+
symbol
25+
);
26+
} catch (err) {
27+
throw new PolymathError({
28+
code: ErrorCode.ProcedureValidationError,
29+
message: `There is no Security Token with symbol ${symbol}`,
30+
});
31+
}
32+
33+
const tokenAddress = await securityToken.address();
34+
const moduleName = ModuleName.PercentageTransferManager;
35+
36+
const moduleAddress = await contractWrappers.getModuleFactoryAddress({
37+
tokenAddress,
38+
moduleName,
39+
});
40+
41+
await this.addTransaction<TransactionParams.SecurityToken.AddPercentageTransferManager>(
42+
securityToken.addModuleWithLabel,
43+
{
44+
tag: PolyTransactionTag.EnablePercentageTransferManager,
45+
}
46+
)({
47+
moduleName,
48+
address: moduleAddress,
49+
archived: false,
50+
data: {
51+
maxHolderPercentage,
52+
allowPrimaryIssuance,
53+
},
54+
});
55+
}
56+
}

src/procedures/ModifyMaxHolderCount.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { PolymathError } from '../PolymathError';
1010

1111
export class ModifyMaxHolderCount extends Procedure<ModifyMaxHolderCountProcedureArgs> {
12-
public type = ProcedureType.SetDividendsWallet;
12+
public type = ProcedureType.ModifyMaxHolderCount;
1313

1414
public async prepareTransactions() {
1515
const { symbol, maxHolderCount } = this.args;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ModuleName } from '@polymathnetwork/contract-wrappers';
2+
import { Procedure } from './Procedure';
3+
import {
4+
ProcedureType,
5+
PolyTransactionTag,
6+
ErrorCode,
7+
ModifyMaxHolderPercentageProcedureArgs,
8+
} from '../types';
9+
import { PolymathError } from '../PolymathError';
10+
11+
export class ModifyMaxHolderPercentage extends Procedure<ModifyMaxHolderPercentageProcedureArgs> {
12+
public type = ProcedureType.ModifyMaxHolderPercentage;
13+
14+
public async prepareTransactions() {
15+
const { symbol, maxHolderPercentage } = this.args;
16+
const { contractWrappers } = this.context;
17+
18+
try {
19+
await contractWrappers.tokenFactory.getSecurityTokenInstanceFromTicker(symbol);
20+
} catch (err) {
21+
throw new PolymathError({
22+
code: ErrorCode.ProcedureValidationError,
23+
message: `There is no Security Token with symbol ${symbol}`,
24+
});
25+
}
26+
27+
const percentageTransferManagerModule = (await contractWrappers.getAttachedModules(
28+
{ moduleName: ModuleName.PercentageTransferManager, symbol },
29+
{ unarchived: true }
30+
))[0];
31+
32+
if (!percentageTransferManagerModule) {
33+
throw new PolymathError({
34+
code: ErrorCode.ProcedureValidationError,
35+
message: 'You must enable the PercentageOwnershipRestrictions Feature',
36+
});
37+
}
38+
39+
await this.addTransaction(percentageTransferManagerModule.changeHolderPercentage, {
40+
tag: PolyTransactionTag.ChangeHolderPercentage,
41+
})({ maxHolderPercentage });
42+
}
43+
}

0 commit comments

Comments
 (0)