Skip to content

Commit 3b1a757

Browse files
committed
Add supply cap allowed contract hook
1 parent 780eeeb commit 3b1a757

File tree

2 files changed

+327
-15
lines changed

2 files changed

+327
-15
lines changed

contracts/hooks/SupplyCapAllowedCallerIssuanceHook.sol

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import { ISetToken } from "../interfaces/ISetToken.sol";
2929
* @title SupplyCapAllowedCallerIssuanceHook
3030
* @author Set Protocol
3131
*
32-
* Issuance hook that checks new issuances won't push SetToken totalSupply over supply cap and checks if caller is allowed
32+
* Issuance hook that checks
33+
* 1) New issuances won't push SetToken totalSupply over supply cap
34+
* 2) A contract address is allowed to call the module. This does not apply if caller is an EOA
3335
*/
3436
contract SupplyCapAllowedCallerIssuanceHook is Ownable, IManagerIssuanceHook {
3537
using SafeMath for uint256;
@@ -49,7 +51,7 @@ contract SupplyCapAllowedCallerIssuanceHook is Ownable, IManagerIssuanceHook {
4951
// Boolean indicating if anyone can call function
5052
bool public anyoneCallable;
5153

52-
// Mapping of addresses allowed to call function
54+
// Mapping of contract addresses allowed to call function
5355
mapping(address => bool) public callAllowList;
5456

5557
/* ============ Constructor ============ */
@@ -86,26 +88,24 @@ contract SupplyCapAllowedCallerIssuanceHook is Ownable, IManagerIssuanceHook {
8688
external
8789
override
8890
{
91+
_validateAllowedContractCaller(_sender);
92+
8993
uint256 totalSupply = _setToken.totalSupply();
9094
require(totalSupply.add(_issueQuantity) <= supplyCap, "Supply cap exceeded");
91-
92-
_validateAllowedCaller(_sender);
9395
}
9496

9597
/**
9698
* Adheres to IManagerIssuanceHook interface
9799
*/
98100
function invokePreRedeemHook(
99-
ISetToken /* _setToken */,
100-
uint256 /* _redeemQuantity */,
101+
ISetToken _setToken,
102+
uint256 _redeemQuantity,
101103
address _sender,
102-
address /* _to */
104+
address _to
103105
)
104106
external
105107
override
106-
{
107-
_validateAllowedCaller(_sender);
108-
}
108+
{}
109109

110110
/**
111111
* ONLY OWNER: Updates supply cap
@@ -147,11 +147,13 @@ contract SupplyCapAllowedCallerIssuanceHook is Ownable, IManagerIssuanceHook {
147147
/* ============ Internal Functions ============ */
148148

149149
/**
150-
* Validate if passed address is allowed to call function. If anyoneCallable set to true anyone can call otherwise needs to be approved or an EOA.
150+
* Validate if passed address is allowed to call function. If anyoneCallable is set to true, anyone can call otherwise needs to be an EOA or
151+
* approved contract address.
151152
*/
152-
function _validateAllowedCaller(address _caller) internal view {
153-
bool isEOA = msg.sender == tx.origin;
154-
155-
require(anyoneCallable || isEOA || callAllowList[_caller], "Address not permitted to call");
153+
function _validateAllowedContractCaller(address _caller) internal view {
154+
require(
155+
_caller == tx.origin || anyoneCallable || callAllowList[_caller],
156+
"Contract not permitted to call"
157+
);
156158
}
157159
}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import "module-alias/register";
2+
3+
import { Address, Account } from "@utils/types";
4+
import { ZERO } from "@utils/constants";
5+
import { SupplyCapAllowedCallerIssuanceHook } from "@utils/contracts/index";
6+
import { ContractCallerMock, SetToken } from "@utils/contracts/setV2";
7+
import DeployHelper from "@utils/deploys";
8+
import {
9+
addSnapshotBeforeRestoreAfterEach,
10+
ether,
11+
getAccounts,
12+
getSetFixture,
13+
getWaffleExpect,
14+
getRandomAccount,
15+
} from "@utils/index";
16+
import { SetFixture } from "@utils/fixtures";
17+
import { BigNumber, ContractTransaction } from "ethers";
18+
19+
const expect = getWaffleExpect();
20+
21+
describe("SupplyCapAllowedCallerIssuanceHook", () => {
22+
let owner: Account;
23+
let hookOwner: Account;
24+
let otherAccount: Account;
25+
let setV2Setup: SetFixture;
26+
27+
let deployer: DeployHelper;
28+
let setToken: SetToken;
29+
30+
let issuanceHook: SupplyCapAllowedCallerIssuanceHook;
31+
32+
before(async () => {
33+
[
34+
owner,
35+
hookOwner,
36+
otherAccount,
37+
] = await getAccounts();
38+
39+
deployer = new DeployHelper(owner.wallet);
40+
41+
setV2Setup = getSetFixture(owner.address);
42+
await setV2Setup.initialize();
43+
44+
setToken = await setV2Setup.createSetToken(
45+
[setV2Setup.dai.address],
46+
[ether(1)],
47+
[setV2Setup.debtIssuanceModule.address]
48+
);
49+
});
50+
51+
addSnapshotBeforeRestoreAfterEach();
52+
53+
describe("#constructor", async () => {
54+
let subjectOwner: Address;
55+
let subjectSupplyCap: BigNumber;
56+
57+
beforeEach(async () => {
58+
subjectOwner = hookOwner.address;
59+
subjectSupplyCap = ether(10);
60+
});
61+
62+
async function subject(): Promise<SupplyCapAllowedCallerIssuanceHook> {
63+
return await deployer.hooks.deploySupplyCapAllowedCallerIssuanceHook(subjectOwner, subjectSupplyCap);
64+
}
65+
66+
it("should set the correct SetToken address", async () => {
67+
const hook = await subject();
68+
69+
const actualSupplyCap = await hook.supplyCap();
70+
expect(actualSupplyCap).to.eq(subjectSupplyCap);
71+
});
72+
73+
it("should set the correct owner address", async () => {
74+
const hook = await subject();
75+
76+
const actualOwner = await hook.owner();
77+
expect(actualOwner).to.eq(subjectOwner);
78+
});
79+
});
80+
81+
describe("#invokePreIssueHook", async () => {
82+
let subjectSetToken: Address;
83+
let subjectQuantity: BigNumber;
84+
let subjectTo: Address;
85+
86+
beforeEach(async () => {
87+
issuanceHook = await deployer.hooks.deploySupplyCapAllowedCallerIssuanceHook(owner.address, ether(10));
88+
89+
await setV2Setup.debtIssuanceModule.initialize(
90+
setToken.address,
91+
ether(.1),
92+
ether(.01),
93+
ether(.01),
94+
owner.address,
95+
issuanceHook.address
96+
);
97+
98+
await setV2Setup.dai.approve(setV2Setup.debtIssuanceModule.address, ether(100));
99+
100+
subjectSetToken = setToken.address;
101+
subjectQuantity = ether(5);
102+
subjectTo = owner.address;
103+
});
104+
105+
async function subject(): Promise<ContractTransaction> {
106+
return await setV2Setup.debtIssuanceModule.issue(
107+
subjectSetToken,
108+
subjectQuantity,
109+
subjectTo
110+
);
111+
}
112+
113+
it("should not revert", async () => {
114+
await expect(subject()).to.not.be.reverted;
115+
});
116+
117+
describe("when total issuance quantity forces supply over the limit", async () => {
118+
beforeEach(async () => {
119+
subjectQuantity = ether(11);
120+
});
121+
122+
it("should revert", async () => {
123+
await expect(subject()).to.be.revertedWith("Supply cap exceeded");
124+
});
125+
});
126+
127+
describe("when the sender is not EOA and on allowlist", async () => {
128+
let subjectTarget: Address;
129+
let subjectCallData: string;
130+
let subjectValue: BigNumber;
131+
132+
let contractCaller: ContractCallerMock;
133+
134+
beforeEach(async () => {
135+
contractCaller = await deployer.setV2.deployContractCallerMock();
136+
await issuanceHook.updateCallerStatus([contractCaller.address], [true]);
137+
138+
await setV2Setup.dai.transfer(contractCaller.address, ether(50));
139+
// Approve token from contract caller to issuance module
140+
const approveData = setV2Setup.dai.interface.encodeFunctionData("approve", [
141+
setV2Setup.debtIssuanceModule.address,
142+
ether(100),
143+
]);
144+
await contractCaller.invoke(setV2Setup.dai.address, ZERO, approveData);
145+
146+
subjectSetToken = setToken.address;
147+
subjectQuantity = ether(5);
148+
subjectTo = owner.address;
149+
150+
subjectTarget = setV2Setup.debtIssuanceModule.address;
151+
subjectCallData = setV2Setup.debtIssuanceModule.interface.encodeFunctionData("issue", [
152+
subjectSetToken,
153+
subjectQuantity,
154+
subjectTo,
155+
]);
156+
157+
subjectValue = ZERO;
158+
});
159+
160+
async function subjectContractCaller(): Promise<any> {
161+
return await contractCaller.invoke(
162+
subjectTarget,
163+
subjectValue,
164+
subjectCallData
165+
);
166+
}
167+
168+
it("should not revert", async () => {
169+
await expect(subjectContractCaller()).to.not.be.reverted;
170+
});
171+
172+
describe("when the caller is not on allowlist", async () => {
173+
beforeEach(async () => {
174+
await issuanceHook.updateCallerStatus([contractCaller.address], [false]);
175+
});
176+
177+
it("should revert", async () => {
178+
await expect(subjectContractCaller()).to.be.revertedWith("Contract not permitted to call");
179+
});
180+
181+
describe("when anyoneCallable is flipped to true", async () => {
182+
beforeEach(async () => {
183+
await issuanceHook.updateAnyoneCallable(true);
184+
});
185+
186+
it("should succeed without revert", async () => {
187+
await subjectContractCaller();
188+
});
189+
});
190+
});
191+
});
192+
});
193+
194+
describe("#updateSupplyCap", async () => {
195+
let subjectNewCap: BigNumber;
196+
let subjectCaller: Account;
197+
198+
beforeEach(async () => {
199+
issuanceHook = await deployer.hooks.deploySupplyCapAllowedCallerIssuanceHook(owner.address, ether(10));
200+
201+
subjectNewCap = ether(20);
202+
subjectCaller = owner;
203+
});
204+
205+
async function subject(): Promise<ContractTransaction> {
206+
return await issuanceHook.connect(subjectCaller.wallet).updateSupplyCap(subjectNewCap);
207+
}
208+
209+
it("should update supply cap", async () => {
210+
await subject();
211+
212+
const actualCap = await issuanceHook.supplyCap();
213+
214+
expect(actualCap).to.eq(subjectNewCap);
215+
});
216+
217+
it("should emit the correct SupplyCapUpdated event", async () => {
218+
await expect(subject()).to.emit(issuanceHook, "SupplyCapUpdated").withArgs(subjectNewCap);
219+
});
220+
221+
describe("when caller is not owner", async () => {
222+
beforeEach(async () => {
223+
subjectCaller = await getRandomAccount();
224+
});
225+
226+
it("should revert", async () => {
227+
await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner");
228+
});
229+
});
230+
});
231+
232+
describe("#updateCallerStatus", async () => {
233+
let subjectFunctionCallers: Address[];
234+
let subjectStatuses: boolean[];
235+
let subjectCaller: Account;
236+
237+
beforeEach(async () => {
238+
issuanceHook = await deployer.hooks.deploySupplyCapAllowedCallerIssuanceHook(owner.address, ether(10));
239+
240+
subjectFunctionCallers = [otherAccount.address];
241+
subjectStatuses = [true];
242+
subjectCaller = owner;
243+
});
244+
245+
async function subject(): Promise<ContractTransaction> {
246+
return issuanceHook.connect(subjectCaller.wallet).updateCallerStatus(subjectFunctionCallers, subjectStatuses);
247+
}
248+
249+
it("should update the callAllowList", async () => {
250+
await subject();
251+
const callerStatus = await issuanceHook.callAllowList(subjectFunctionCallers[0]);
252+
expect(callerStatus).to.be.true;
253+
});
254+
255+
it("should emit CallerStatusUpdated event", async () => {
256+
await expect(subject()).to.emit(issuanceHook, "CallerStatusUpdated").withArgs(
257+
subjectFunctionCallers[0],
258+
subjectStatuses[0]
259+
);
260+
});
261+
262+
describe("when the sender is not owner", async () => {
263+
beforeEach(async () => {
264+
subjectCaller = await getRandomAccount();
265+
});
266+
267+
it("should revert", async () => {
268+
await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner");
269+
});
270+
});
271+
});
272+
273+
describe("#updateAnyoneCallable", async () => {
274+
let subjectStatus: boolean;
275+
let subjectCaller: Account;
276+
277+
beforeEach(async () => {
278+
issuanceHook = await deployer.hooks.deploySupplyCapAllowedCallerIssuanceHook(owner.address, ether(10));
279+
280+
subjectStatus = true;
281+
subjectCaller = owner;
282+
});
283+
284+
async function subject(): Promise<ContractTransaction> {
285+
return issuanceHook.connect(subjectCaller.wallet).updateAnyoneCallable(subjectStatus);
286+
}
287+
288+
it("should update the anyoneCallable boolean", async () => {
289+
await subject();
290+
const callerStatus = await issuanceHook.anyoneCallable();
291+
expect(callerStatus).to.be.true;
292+
});
293+
294+
it("should emit AnyoneCallableUpdated event", async () => {
295+
await expect(subject()).to.emit(issuanceHook, "AnyoneCallableUpdated").withArgs(
296+
subjectStatus
297+
);
298+
});
299+
300+
describe("when the sender is not owner", async () => {
301+
beforeEach(async () => {
302+
subjectCaller = await getRandomAccount();
303+
});
304+
305+
it("should revert", async () => {
306+
await expect(subject()).to.be.revertedWith("Ownable: caller is not the owner");
307+
});
308+
});
309+
});
310+
});

0 commit comments

Comments
 (0)