Skip to content

Commit 16d557c

Browse files
authored
BaseManager IIP-64/72 Internal audit fixes (#74)
* Prohibit calling SetToken with #interactManager / add #transferTokens * Fix javadoc for BaseManagerV2#emergencyReplaceProtectedModule * Remove unused param in FeeSplitExtension#initializeIssuanceModule * Add BaseMangerV2 EmergencyResolved event * Add missing parameter documentation for FeeSplitExtension / StreamingFeeSplitExtension * Add explanatory notes about timelocking in extension methods
1 parent 3056839 commit 16d557c

File tree

7 files changed

+158
-15
lines changed

7 files changed

+158
-15
lines changed

contracts/adapters/FeeSplitExtension.sol

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,14 @@ contract FeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpgrade {
117117
*
118118
* This method is called after invoking `replaceProtectedModule` or `emergencyReplaceProtectedModule`
119119
* to configure the replacement streaming fee module's fee settings.
120+
*
121+
* @param _maxManagerFee Max size of issuance and redeem fees in precise units (10^16 = 1%).
122+
* @param _managerIssueFee Manager issuance fees in precise units (10^16 = 1%)
123+
* @param _managerRedeemFee Manager redeem fees in precise units (10^16 = 1%)
124+
* @param _feeRecipient Address that receives all manager issue and redeem fees
125+
* @param _managerIssuanceHook Address of manager defined hook contract
120126
*/
121127
function initializeIssuanceModule(
122-
ISetToken _setToken,
123128
uint256 _maxManagerFee,
124129
uint256 _managerIssueFee,
125130
uint256 _managerRedeemFee,
@@ -148,6 +153,17 @@ contract FeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpgrade {
148153
*
149154
* This method is called after invoking `replaceProtectedModule` or `emergencyReplaceProtectedModule`
150155
* to configure the replacement streaming fee module's fee settings.
156+
*
157+
* @dev FeeState settings encode the following struct
158+
* ```
159+
* struct FeeState {
160+
* address feeRecipient; // Address to accrue fees to
161+
* uint256 maxStreamingFeePercentage; // Max streaming fee maanager commits to using (1% = 1e16, 100% = 1e18)
162+
* uint256 streamingFeePercentage; // Percent of Set accruing to manager annually (1% = 1e16, 100% = 1e18)
163+
* uint256 lastStreamingFeeTimestamp; // Timestamp last streaming fee was accrued
164+
*}
165+
*```
166+
* @param _settings FeeModule.FeeState settings
151167
*/
152168
function initializeStreamingFeeModule(IStreamingFeeModule.FeeState memory _settings)
153169
external
@@ -167,7 +183,12 @@ contract FeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpgrade {
167183
* this function to execute the update. Because the method is timelocked, each party must call it twice:
168184
* once to set the lock and once to execute.
169185
*
186+
* Method is timelocked to protect token owners from sudden changes in fee structure which
187+
* they would rather not bear. The delay gives them a chance to exit their positions without penalty.
188+
*
170189
* NOTE: This will accrue streaming fees though not send to operator fee recipient and methodologist.
190+
*
191+
* @param _newFee Percent of Set accruing to fee extension annually (1% = 1e16, 100% = 1e18)
171192
*/
172193
function updateStreamingFee(uint256 _newFee)
173194
external
@@ -182,6 +203,11 @@ contract FeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpgrade {
182203
* MUTUAL UPGRADE: Updates issue fee on IssuanceModule. Only is executed once time lock has passed.
183204
* Operator and Methodologist must each call this function to execute the update. Because the method
184205
* is timelocked, each party must call it twice: once to set the lock and once to execute.
206+
*
207+
* Method is timelocked to protect token owners from sudden changes in fee structure which
208+
* they would rather not bear. The delay gives them a chance to exit their positions without penalty.
209+
*
210+
* @param _newFee New issue fee percentage in precise units (1% = 1e16, 100% = 1e18)
185211
*/
186212
function updateIssueFee(uint256 _newFee)
187213
external
@@ -196,6 +222,11 @@ contract FeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpgrade {
196222
* MUTUAL UPGRADE: Updates redeem fee on IssuanceModule. Only is executed once time lock has passed.
197223
* Operator and Methodologist must each call this function to execute the update. Because the method is
198224
* timelocked, each party must call it twice: once to set the lock and once to execute.
225+
*
226+
* Method is timelocked to protect token owners from sudden changes in fee structure which
227+
* they would rather not bear. The delay gives them a chance to exit their positions without penalty.
228+
*
229+
* @param _newFee New redeem fee percentage in precise units (1% = 1e16, 100% = 1e18)
199230
*/
200231
function updateRedeemFee(uint256 _newFee)
201232
external
@@ -208,6 +239,8 @@ contract FeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpgrade {
208239

209240
/**
210241
* MUTUAL UPGRADE: Updates fee recipient on both streaming fee and issuance modules.
242+
*
243+
* @param _newFeeRecipient Address of new fee recipient. This should be the address of the fee extension itself.
211244
*/
212245
function updateFeeRecipient(address _newFeeRecipient)
213246
external
@@ -220,6 +253,8 @@ contract FeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpgrade {
220253

221254
/**
222255
* MUTUAL UPGRADE: Updates fee split between operator and methodologist. Split defined in precise units (1% = 10^16).
256+
*
257+
* @param _newFeeSplit Percent of fees in precise units (10^16 = 1%) sent to operator, (rest go to the methodologist).
223258
*/
224259
function updateFeeSplit(uint256 _newFeeSplit)
225260
external
@@ -232,6 +267,8 @@ contract FeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpgrade {
232267

233268
/**
234269
* OPERATOR ONLY: Updates the address that receives the operator's share of the fees (see IIP-72)
270+
*
271+
* @param _newOperatorFeeRecipient Address to send operator's fees to.
235272
*/
236273
function updateOperatorFeeRecipient(address _newOperatorFeeRecipient)
237274
external

contracts/adapters/StreamingFeeSplitExtension.sol

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ contract StreamingFeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpg
113113
*
114114
* This method is called after invoking `replaceProtectedModule` or `emergencyReplaceProtectedModule`
115115
* to configure the replacement streaming fee module's fee settings.
116+
*
117+
* @dev FeeState settings encode the following struct
118+
* ```
119+
* struct FeeState {
120+
* address feeRecipient; // Address to accrue fees to
121+
* uint256 maxStreamingFeePercentage; // Max streaming fee maanager commits to using (1% = 1e16, 100% = 1e18)
122+
* uint256 streamingFeePercentage; // Percent of Set accruing to manager annually (1% = 1e16, 100% = 1e18)
123+
* uint256 lastStreamingFeeTimestamp; // Timestamp last streaming fee was accrued
124+
*}
125+
*```
126+
* @param _settings FeeModule.FeeState settings
116127
*/
117128
function initializeModule(IStreamingFeeModule.FeeState memory _settings)
118129
external
@@ -132,7 +143,12 @@ contract StreamingFeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpg
132143
* each call this function to execute the update. Because the method is timelocked, each party
133144
* must call it twice: once to set the lock and once to execute.
134145
*
146+
* Method is timelocked to protect token owners from sudden changes in fee structure which
147+
* they would rather not bear. The delay gives them a chance to exit their positions without penalty.
148+
*
135149
* NOTE: This will accrue streaming fees though not send to operator fee recipient and methodologist.
150+
*
151+
* @param _newFee Percent of Set accruing to fee extension annually (1% = 1e16, 100% = 1e18)
136152
*/
137153
function updateStreamingFee(uint256 _newFee)
138154
external
@@ -150,6 +166,8 @@ contract StreamingFeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpg
150166

151167
/**
152168
* MUTUAL UPGRADE: Updates fee recipient on streaming fee module.
169+
*
170+
* @param _newFeeRecipient Address of new fee recipient. This should be the address of the fee extension itself.
153171
*/
154172
function updateFeeRecipient(address _newFeeRecipient)
155173
external
@@ -165,8 +183,10 @@ contract StreamingFeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpg
165183
}
166184

167185
/**
168-
* MUTUAL UPGRADE: Updates fee split between operator and methodologist. Split defined in precise units (1% = 10^16). Fees will be
169-
* accrued and distributed before the new split goes into effect.
186+
* MUTUAL UPGRADE: Updates fee split between operator and methodologist. Split defined in precise units (1% = 10^16).
187+
* Fees will be accrued and distributed before the new split goes into effect.
188+
*
189+
* @param _newFeeSplit Percent of fees in precise units (10^16 = 1%) sent to operator, (rest go to the methodologist).
170190
*/
171191
function updateFeeSplit(uint256 _newFeeSplit)
172192
external
@@ -179,6 +199,8 @@ contract StreamingFeeSplitExtension is BaseExtension, TimeLockUpgrade, MutualUpg
179199

180200
/**
181201
* OPERATOR ONLY: Updates the address that receives the operator's share of the fees (see IIP-72)
202+
*
203+
* @param _newOperatorFeeRecipient Address to send operator's fees to.
182204
*/
183205
function updateOperatorFeeRecipient(address _newOperatorFeeRecipient)
184206
external

contracts/interfaces/IBaseManager.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import { ISetToken } from "./ISetToken.sol";
2121

2222
interface IBaseManager {
2323
function setToken() external returns(ISetToken);
24-
24+
2525
function methodologist() external returns(address);
2626

2727
function operator() external returns(address);
2828

2929
function interactManager(address _module, bytes calldata _encoded) external;
30+
31+
function transferTokens(address _token, address _destination, uint256 _amount) external;
3032
}

contracts/lib/BaseExtension.sol

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,7 @@ abstract contract BaseExtension {
122122
* @param _amount Amount of token being transferred
123123
*/
124124
function invokeManagerTransfer(address _token, address _destination, uint256 _amount) internal {
125-
bytes memory callData = abi.encodeWithSignature("transfer(address,uint256)", _destination, _amount);
126-
invokeManager(_token, callData);
125+
manager.transferTokens(_token, _destination, _amount);
127126
}
128127

129128
/**

contracts/manager/BaseManagerV2.sol

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ pragma solidity 0.6.10;
1818
pragma experimental ABIEncoderV2;
1919

2020
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
21+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
22+
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
2123

2224
import { AddressArrayUtils } from "../lib/AddressArrayUtils.sol";
2325
import { IExtension } from "../interfaces/IExtension.sol";
@@ -35,6 +37,7 @@ import { MutualUpgrade } from "../lib/MutualUpgrade.sol";
3537
contract BaseManagerV2 is MutualUpgrade {
3638
using Address for address;
3739
using AddressArrayUtils for address[];
40+
using SafeERC20 for IERC20;
3841

3942
/* ============ Struct ========== */
4043

@@ -98,6 +101,8 @@ contract BaseManagerV2 is MutualUpgrade {
98101
address _module
99102
);
100103

104+
event EmergencyResolved();
105+
101106
/* ============ Modifiers ============ */
102107

103108
/**
@@ -292,12 +297,26 @@ contract BaseManagerV2 is MutualUpgrade {
292297
*/
293298
function interactManager(address _module, bytes memory _data) external onlyExtension {
294299
require(initialized, "Manager not initialized");
300+
require(_module != address(setToken), "Extensions cannot call SetToken");
295301
require(_senderAuthorizedForModule(_module, msg.sender), "Extension not authorized for module");
296302

297303
// Invoke call to module, assume value will always be 0
298304
_module.functionCallWithValue(_data, 0);
299305
}
300306

307+
/**
308+
* OPERATOR ONLY: Transfers _tokens held by the manager to _destination. Can be used to
309+
* recover anything sent here accidentally. In BaseManagerV2, extensions should
310+
* be the only contracts designated as `feeRecipient` in fee modules.
311+
*
312+
* @param _token ERC20 token to send
313+
* @param _destination Address receiving the tokens
314+
* @param _amount Quantity of tokens to send
315+
*/
316+
function transferTokens(address _token, address _destination, uint256 _amount) external onlyExtension {
317+
IERC20(_token).safeTransfer(_destination, _amount);
318+
}
319+
301320
/**
302321
* OPERATOR ONLY: Add a new module to the SetToken.
303322
*
@@ -341,8 +360,8 @@ contract BaseManagerV2 is MutualUpgrade {
341360
}
342361

343362
/**
344-
* OPERATOR ONLY: Marks an existing module as protected and authorizes existing extensions for
345-
* it. Adds module to the protected modules list
363+
* OPERATOR ONLY: Marks an existing module as protected and authorizes extensions for
364+
* it, adding them if necessary. Adds module to the protected modules list
346365
*
347366
* The operator uses this when they're adding new features and want to assure the methodologist
348367
* they won't be unilaterally changed in the future. Cannot be called during an emergency because
@@ -431,9 +450,9 @@ contract BaseManagerV2 is MutualUpgrade {
431450
*
432451
* NOTE: If replacing a fee module, it's necessary to set the module `feeRecipient` to be
433452
* the new fee extension address after this call. Any fees remaining in the old module's
434-
* de-authorized extensions can be distributed by calling `distribute()` on the old extension.
453+
* de-authorized extensions can be distributed by calling `accrueFeesAndDistribute` on the old extension.
435454
*
436-
* @param _module Module to add in place of removed module
455+
* @param _module Module to add in place of removed module
437456
* @param _extensions Array of extensions to authorize for replacement module
438457
*/
439458
function emergencyReplaceProtectedModule(
@@ -461,6 +480,8 @@ contract BaseManagerV2 is MutualUpgrade {
461480
*/
462481
function resolveEmergency() external onlyMethodologist onlyEmergency {
463482
emergencies -= 1;
483+
484+
emit EmergencyResolved();
464485
}
465486

466487
/**

test/adapters/feeSplitExtension.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,6 @@ describe("FeeSplitExtension", () => {
387387

388388
async function subject(caller: Account): Promise<ContractTransaction> {
389389
return await subjectExtension.connect(caller.wallet).initializeIssuanceModule(
390-
subjectSetToken,
391390
subjectMaxManagerFee,
392391
subjectManagerIssueFee,
393392
subjectManagerRedeemFee,

test/manager/baseManagerV2.spec.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,10 @@ describe("BaseManagerV2", () => {
13741374
expect(initialEmergencies.toNumber()).equals(1);
13751375
expect(finalEmergencies.toNumber()).equals(0);
13761376
});
1377+
1378+
it("should emit the correct EmergencyResolved event", async () => {
1379+
await expect(subject()).to.emit(baseManager, "EmergencyResolved");
1380+
});
13771381
});
13781382

13791383
describe("when an emergency is *not* in progress", async () => {
@@ -1412,7 +1416,7 @@ describe("BaseManagerV2", () => {
14121416
});
14131417

14141418
async function subject(): Promise<any> {
1415-
return baseExtension.interactManager(subjectModule, subjectCallData);
1419+
return baseExtension.connect(operator.wallet).interactManager(subjectModule, subjectCallData);
14161420
}
14171421

14181422
context("when the manager is initialized", () => {
@@ -1435,11 +1439,21 @@ describe("BaseManagerV2", () => {
14351439
await expect(subject()).to.be.revertedWith("Must be extension");
14361440
});
14371441
});
1442+
1443+
describe("when the extension tries to call the SetToken", async () => {
1444+
beforeEach(async () => {
1445+
subjectModule = setToken.address;
1446+
});
1447+
1448+
it("should revert", async () => {
1449+
await expect(subject()).to.be.revertedWith("Extensions cannot call SetToken");
1450+
});
1451+
});
14381452
});
14391453

14401454
context("when the manager is not initialized", () => {
14411455
it("updateFeeRecipient should revert", async () => {
1442-
expect(subject()).to.be.revertedWith("Manager not initialized");
1456+
await expect(subject()).to.be.revertedWith("Manager not initialized");
14431457
});
14441458
});
14451459

@@ -1468,6 +1482,55 @@ describe("BaseManagerV2", () => {
14681482
});
14691483
});
14701484

1485+
describe("#transferTokens", async () => {
1486+
let subjectCaller: Account;
1487+
let subjectToken: Address;
1488+
let subjectDestination: Address;
1489+
let subjectAmount: BigNumber;
1490+
1491+
beforeEach(async () => {
1492+
await baseManager.connect(operator.wallet).addExtension(baseExtension.address);
1493+
1494+
subjectCaller = operator;
1495+
subjectToken = setV2Setup.weth.address;
1496+
subjectDestination = otherAccount.address;
1497+
subjectAmount = ether(1);
1498+
1499+
await setV2Setup.weth.transfer(baseManager.address, subjectAmount);
1500+
});
1501+
1502+
async function subject(): Promise<ContractTransaction> {
1503+
return baseExtension.connect(subjectCaller.wallet).testInvokeManagerTransfer(
1504+
subjectToken,
1505+
subjectDestination,
1506+
subjectAmount
1507+
);
1508+
}
1509+
1510+
it("should send the given amount from the manager to the address", async () => {
1511+
const preManagerAmount = await setV2Setup.weth.balanceOf(baseManager.address);
1512+
const preDestinationAmount = await setV2Setup.weth.balanceOf(subjectDestination);
1513+
1514+
await subject();
1515+
1516+
const postManagerAmount = await setV2Setup.weth.balanceOf(baseManager.address);
1517+
const postDestinationAmount = await setV2Setup.weth.balanceOf(subjectDestination);
1518+
1519+
expect(preManagerAmount.sub(postManagerAmount)).to.eq(subjectAmount);
1520+
expect(postDestinationAmount.sub(preDestinationAmount)).to.eq(subjectAmount);
1521+
});
1522+
1523+
describe("when the caller is not an extension", async () => {
1524+
beforeEach(async () => {
1525+
await baseManager.connect(operator.wallet).removeExtension(baseExtension.address);
1526+
});
1527+
1528+
it("should revert", async () => {
1529+
await expect(subject()).to.be.revertedWith("Must be extension");
1530+
});
1531+
});
1532+
});
1533+
14711534
describe("#removeModule", async () => {
14721535
let subjectModule: Address;
14731536
let subjectExtension: Address;
@@ -1500,8 +1563,8 @@ describe("BaseManagerV2", () => {
15001563
});
15011564

15021565
describe("when the module is protected module", () => {
1503-
beforeEach(() => {
1504-
baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
1566+
beforeEach(async () => {
1567+
await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]);
15051568
});
15061569

15071570
it("should revert", async() => {

0 commit comments

Comments
 (0)