-
Notifications
You must be signed in to change notification settings - Fork 15
/
Controller.sol
310 lines (258 loc) · 13 KB
/
Controller.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.24;
import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol";
import { Factory } from "@equilibria/root/attribute/Factory.sol";
import { Token6 } from "@equilibria/root/token/types/Token6.sol";
import { Token18 } from "@equilibria/root/token/types/Token18.sol";
import { Fixed6, Fixed6Lib } from "@equilibria/root/number/types/Fixed6.sol";
import { UFixed6, UFixed6Lib } from "@equilibria/root/number/types/UFixed6.sol";
import { IMarketFactory } from "@perennial/core/contracts/interfaces/IMarketFactory.sol";
import { IAccount, IMarket } from "./interfaces/IAccount.sol";
import { IAccountVerifier, IController } from "./interfaces/IController.sol";
import { RebalanceLib } from "./libs/RebalanceLib.sol";
import { Account } from "./Account.sol";
import { DeployAccount, DeployAccountLib } from "./types/DeployAccount.sol";
import { MarketTransfer, MarketTransferLib } from "./types/MarketTransfer.sol";
import { RebalanceConfig, RebalanceConfigLib } from "./types/RebalanceConfig.sol";
import { RebalanceConfigChange, RebalanceConfigChangeLib } from "./types/RebalanceConfigChange.sol";
import { Withdrawal, WithdrawalLib } from "./types/Withdrawal.sol";
/// @title Controller
/// @notice Facilitates unpermissioned actions between collateral accounts and markets,
/// without keeper compensation. No message relaying facilities are provided.
contract Controller is Factory, IController {
// used for deterministic address creation through create2
bytes32 constant SALT = keccak256("Perennial V2 Collateral Accounts");
uint256 constant MAX_GROUPS_PER_OWNER = 8;
uint256 constant MAX_MARKETS_PER_GROUP = 4;
/// @dev USDC stablecoin address
Token6 public immutable USDC; // solhint-disable-line var-name-mixedcase
/// @dev DSU address
Token18 public immutable DSU; // solhint-disable-line var-name-mixedcase
/// @inheritdoc IController
IMarketFactory public immutable marketFactory;
/// @inheritdoc IController
IAccountVerifier public verifier;
/// @dev Mapping of rebalance configuration
/// owner => group => market => config
mapping(address => mapping(uint256 => mapping(address => RebalanceConfig))) private _rebalanceConfigs;
/// @dev Prevents markets from being added to multiple rebalance groups
/// owner => market => group
mapping(address => mapping(address => uint256)) public marketToGroup;
/// @dev Allows iteration through markets in a rebalance group
/// owner => group => markets
mapping(address => mapping(uint256 => IMarket[])) public groupToMarkets;
/// @dev Limits relayer/keeper compensation for rebalancing a group, in DSU
mapping(address => mapping(uint256 => UFixed6)) public groupToMaxRebalanceFee;
/// @dev Creates instance of Controller
/// @param implementation_ Collateral account contract initialized with stablecoin addresses
constructor(address implementation_, IMarketFactory marketFactory_) Factory(implementation_) {
USDC = Account(implementation_).USDC();
DSU = Account(implementation_).DSU();
marketFactory = marketFactory_;
}
/// @inheritdoc IController
function initialize(
IAccountVerifier verifier_
) external initializer(1) {
__Factory__initialize();
verifier = verifier_;
}
/// @inheritdoc IController
function getAccountAddress(address owner) public view returns (address) {
// calculate the hash for an uninitialized account for the owner
return _computeCreate2Address(abi.encodeCall(Account.initialize, (owner)), SALT);
}
/// @inheritdoc IController
function changeRebalanceConfigWithSignature(
RebalanceConfigChange calldata configChange,
bytes calldata signature
) virtual external {
_changeRebalanceConfigWithSignature(configChange, signature);
}
/// @inheritdoc IController
function checkGroup(address owner, uint256 group) public view returns (
Fixed6 groupCollateral,
bool canRebalance,
Fixed6[] memory imbalances
) {
// query owner's collateral in each market and calculate sum
Fixed6[] memory actualCollateral;
(actualCollateral, groupCollateral) = _queryMarketCollateral(owner, group);
imbalances = new Fixed6[](actualCollateral.length);
// determine if anything is outside the rebalance threshold
for (uint256 i; i < actualCollateral.length; i++) {
IMarket market = groupToMarkets[owner][group][i];
RebalanceConfig memory marketRebalanceConfig = _rebalanceConfigs[owner][group][address(market)];
(bool canMarketRebalance, Fixed6 imbalance) =
RebalanceLib.checkMarket(
marketRebalanceConfig,
groupToMaxRebalanceFee[owner][group],
groupCollateral,
actualCollateral[i]
);
imbalances[i] = imbalance;
canRebalance = canRebalance || canMarketRebalance;
}
// if group does not exist or was deleted, arrays will be empty and function will return (0, false, 0)
}
/// @inheritdoc IController
function deployAccount() public returns (IAccount) {
return _createAccount(msg.sender);
}
/// @inheritdoc IController
function deployAccountWithSignature(
DeployAccount calldata deployAccountAction,
bytes calldata signature
) virtual external {
_deployAccountWithSignature(deployAccountAction, signature);
}
/// @inheritdoc IController
function marketTransferWithSignature(
MarketTransfer calldata marketTransfer,
bytes calldata signature
) virtual external {
IAccount account = IAccount(getAccountAddress(marketTransfer.action.common.account));
_marketTransferWithSignature(account, marketTransfer, signature);
}
/// @inheritdoc IController
function rebalanceConfigs(
address owner,
uint256 group,
address market
) external view returns (RebalanceConfig memory) {
return _rebalanceConfigs[owner][group][market];
}
/// @inheritdoc IController
function rebalanceGroupMarkets(
address owner,
uint256 group
) external view returns (IMarket[] memory markets) {
markets = groupToMarkets[owner][group];
}
/// @inheritdoc IController
function withdrawWithSignature(Withdrawal calldata withdrawal, bytes calldata signature) virtual external {
IAccount account = IAccount(getAccountAddress(withdrawal.action.common.account));
_withdrawWithSignature(account, withdrawal, signature);
}
/// @inheritdoc IController
function rebalanceGroup(address owner, uint256 group) virtual external {
_rebalanceGroup(owner, group);
}
function _changeRebalanceConfigWithSignature(RebalanceConfigChange calldata configChange, bytes calldata signature) internal {
// ensure the message was signed by the owner or a delegated signer
verifier.verifyRebalanceConfigChange(configChange, signature);
// sum of the target allocations of all markets in the group
_updateRebalanceGroup(configChange, configChange.action.common.account);
}
function _createAccount(address owner) internal returns (IAccount account) {
account = Account(address(_create2(abi.encodeCall(Account.initialize, (owner)), SALT)));
emit AccountDeployed(owner, account);
}
function _deployAccountWithSignature(
DeployAccount calldata deployAccount_,
bytes calldata signature
) internal returns (IAccount account) {
address owner = deployAccount_.action.common.account;
verifier.verifyDeployAccount(deployAccount_, signature);
// create the account
account = _createAccount(owner);
}
function _marketTransferWithSignature(
IAccount account,
MarketTransfer calldata marketTransfer,
bytes calldata signature
) internal {
// ensure the message was signed by the owner or a delegated signer
verifier.verifyMarketTransfer(marketTransfer, signature);
// only Markets with DSU collateral are supported
IMarket market = IMarket(marketTransfer.market);
if (!market.token().eq(DSU)) revert ControllerUnsupportedMarketError(market);
account.marketTransfer(market, marketTransfer.amount);
}
function _withdrawWithSignature(
IAccount account,
Withdrawal calldata withdrawal,
bytes calldata signature
) internal {
// ensure the message was signed by the owner or a delegated signer
verifier.verifyWithdrawal(withdrawal, signature);
// call the account's implementation to push to owner
account.withdraw(withdrawal.amount, withdrawal.unwrap);
}
function _rebalanceGroup(address owner, uint256 group) internal {
// settles each markets, such that locals are up-to-date
_settleMarkets(owner, group);
// determine imbalances
(, bool canRebalance, Fixed6[] memory imbalances) = checkGroup(owner, group);
if (!canRebalance) revert ControllerGroupBalancedError();
IAccount account = IAccount(getAccountAddress(owner));
// pull collateral from markets with surplus collateral
for (uint256 i; i < imbalances.length; i++) {
IMarket market = groupToMarkets[owner][group][i];
if (Fixed6.unwrap(imbalances[i]) < 0) account.marketTransfer(market, imbalances[i]);
}
// push collateral to markets with insufficient collateral
for (uint256 i; i < imbalances.length; i++) {
IMarket market = groupToMarkets[owner][group][i];
if (Fixed6.unwrap(imbalances[i]) > 0) account.marketTransfer(market, imbalances[i]);
}
emit GroupRebalanced(owner, group);
}
/// @dev checks current collateral for each market in a group and aggregates collateral for the group
function _queryMarketCollateral(address owner, uint256 group) private view returns (
Fixed6[] memory actualCollateral,
Fixed6 groupCollateral
) {
actualCollateral = new Fixed6[](groupToMarkets[owner][group].length);
for (uint256 i; i < actualCollateral.length; i++) {
Fixed6 collateral = groupToMarkets[owner][group][i].locals(owner).collateral;
actualCollateral[i] = collateral;
groupCollateral = groupCollateral.add(collateral);
}
}
/// @dev settles each market in a rebalancing group
function _settleMarkets(address owner, uint256 group) private {
for (uint256 i; i < groupToMarkets[owner][group].length; i++)
groupToMarkets[owner][group][i].settle(owner);
}
/// @dev overwrites rebalance configuration of all markets for a particular owner and group
/// @param message already-verified message with new configuration
/// @param owner identifies the owner of the collateral account
function _updateRebalanceGroup(
RebalanceConfigChange calldata message,
address owner
) private {
// ensure group index is valid
if (message.group == 0 || message.group > MAX_GROUPS_PER_OWNER)
revert ControllerInvalidRebalanceGroupError();
if (message.markets.length > MAX_MARKETS_PER_GROUP)
revert ControllerInvalidRebalanceMarketsError();
// delete the existing group
for (uint256 i; i < groupToMarkets[owner][message.group].length; i++) {
address market = address(groupToMarkets[owner][message.group][i]);
delete _rebalanceConfigs[owner][message.group][market];
delete marketToGroup[owner][market];
}
delete groupToMarkets[owner][message.group];
UFixed6 totalAllocation;
for (uint256 i; i < message.markets.length; i++) {
// ensure market is not pointing to a different group
uint256 currentGroup = marketToGroup[owner][message.markets[i]];
if (currentGroup != 0)
revert ControllerMarketAlreadyInGroupError(IMarket(message.markets[i]), currentGroup);
// rewrite over all the old configuration
marketToGroup[owner][message.markets[i]] = message.group;
_rebalanceConfigs[owner][message.group][message.markets[i]] = message.configs[i];
groupToMarkets[owner][message.group].push(IMarket(message.markets[i]));
groupToMaxRebalanceFee[owner][message.group] = message.maxFee;
// ensure target allocation across all markets totals 100%
// read from storage to trap duplicate markets in the message
totalAllocation = totalAllocation.add(message.configs[i].target);
emit RebalanceMarketConfigured(owner, message.group, message.markets[i], message.configs[i]);
}
// if not deleting the group, ensure rebalance targets add to 100%
if (message.markets.length != 0 && !totalAllocation.eq(UFixed6Lib.ONE))
revert ControllerInvalidRebalanceTargetsError();
emit RebalanceGroupConfigured(owner, message.group, message.markets.length);
}
}