Skip to content

Commit d3078df

Browse files
authored
feature: SwapOperator long term limit orders + semantic hooks validations (#1257)
2 parents fb7975b + edd1d89 commit d3078df

25 files changed

+2227
-686
lines changed

.husky/commit-msg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
npx commitlint --edit

.husky/pre-commit

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
3+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
4+
REGEX="^(master|dev|release-candidate|((audit|breaking|build|chore|ci|docs|feat|fix|perf|refactor|release|revert|test|wip)/.+))$"
5+
6+
if ! echo "$BRANCH" | grep -Eq "$REGEX"; then
7+
echo "Your commit was rejected due to invalid branch name: '$BRANCH'"
8+
echo "Please rename your branch to follow one of the accepted formats:"
9+
echo " - audit/*, breaking/*, build/*, chore/*, ci/*, docs/*, feat/*, fix/*, perf/*, refactor/*, release/*, revert/*, test/*, wip/*"
10+
exit 1
11+
fi

.husky/pre-push

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
set -e
3+
4+
npm run lint
5+
6+
if [ "$PRE_PUSH_RUN_TEST" = "true" ]; then
7+
npm test
8+
fi

commitlint.config.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module.exports = {
2+
parserPreset: 'conventional-changelog-conventionalcommits',
3+
rules: {
4+
'body-leading-blank': [1, 'always'],
5+
'footer-leading-blank': [1, 'always'],
6+
'footer-max-line-length': [2, 'always', 100],
7+
'header-max-length': [2, 'always', 100],
8+
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
9+
'subject-empty': [2, 'never'],
10+
'subject-full-stop': [2, 'never', '.'],
11+
'type-case': [2, 'always', 'lower-case'],
12+
'type-empty': [2, 'never'],
13+
'type-enum': [
14+
2,
15+
'always',
16+
['build', 'breaking', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'],
17+
],
18+
},
19+
};

contracts/interfaces/IPool.sol

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ struct Asset {
2020

2121
interface IPool {
2222

23+
error RevertedWithoutReason(uint index);
24+
error AssetNotFound();
25+
error UnknownParameter();
26+
error OrderInProgress();
27+
2328
function swapOperator() external view returns (address);
2429

2530
function getAsset(uint assetId) external view returns (Asset memory);
@@ -52,5 +57,5 @@ interface IPool {
5257

5358
function getMCRRatio() external view returns (uint);
5459

55-
function setSwapValue(uint value) external;
60+
function setSwapAssetAmount(address assetAddress, uint value) external;
5661
}

contracts/mocks/generic/PoolGeneric.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ contract PoolGeneric is IPool {
6969
revert("Unsupported");
7070
}
7171

72-
function setSwapValue(uint) external virtual pure {
72+
function setSwapAssetAmount(address, uint) external virtual pure {
7373
revert("Unsupported");
7474
}
7575
}

contracts/modules/capital/Pool.sol

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ contract Pool is IPool, MasterAwareV2, ReentrancyGuard {
2828
// parameters
2929
IPriceFeedOracle public override priceFeedOracle;
3030
address public swapOperator;
31-
uint96 public swapValue;
31+
32+
// SwapOperator assets
33+
uint32 public assetsInSwapOperatorBitmap;
34+
uint public assetInSwapOperator;
3235

3336
/* constants */
3437

@@ -102,13 +105,13 @@ contract Pool is IPool, MasterAwareV2, ReentrancyGuard {
102105

103106
/* ========== ASSET RELATED VIEW FUNCTIONS ========== */
104107

105-
function getAssetValueInEth(address assetAddress) internal view returns (uint) {
108+
function getAssetValueInEth(address assetAddress, uint assetAmountInSwapOperator) internal view returns (uint) {
106109

107-
uint assetBalance;
110+
uint assetBalance = assetAmountInSwapOperator;
108111

109112
if (assetAddress.code.length != 0) {
110113
try IERC20(assetAddress).balanceOf(address(this)) returns (uint balance) {
111-
assetBalance = balance;
114+
assetBalance += balance;
112115
} catch {
113116
// If balanceOf reverts consider it 0
114117
}
@@ -127,17 +130,29 @@ contract Pool is IPool, MasterAwareV2, ReentrancyGuard {
127130
///
128131
function getPoolValueInEth() public override view returns (uint) {
129132

130-
uint total = address(this).balance + swapValue;
133+
uint total = address(this).balance;
134+
131135
uint assetCount = assets.length;
136+
uint _assetsInSwapOperatorBitmap = assetsInSwapOperatorBitmap;
132137

133-
// Skip ETH (index 0)
134-
for (uint i = 1; i < assetCount; i++) {
138+
for (uint i = 0; i < assetCount; i++) {
139+
Asset memory asset = assets[i];
135140

136-
if (assets[i].isAbandoned) {
141+
if (asset.isAbandoned) {
142+
continue;
143+
}
144+
145+
uint assetAmountInSwapOperator = isAssetInSwapOperator(i, _assetsInSwapOperatorBitmap)
146+
? assetInSwapOperator
147+
: 0;
148+
149+
// check if the asset is ETH and skip the oracle call
150+
if (i == 0) {
151+
total += assetAmountInSwapOperator;
137152
continue;
138153
}
139154

140-
total += getAssetValueInEth(assets[i].assetAddress);
155+
total += getAssetValueInEth(asset.assetAddress, assetAmountInSwapOperator);
141156
}
142157

143158
return total;
@@ -156,6 +171,32 @@ contract Pool is IPool, MasterAwareV2, ReentrancyGuard {
156171
return swapDetails[assetAddress];
157172
}
158173

174+
function getAssetId(address assetAddress) public view returns (uint) {
175+
176+
uint assetCount = assets.length;
177+
for (uint i = 0; i < assetCount; i++) {
178+
if (assets[i].assetAddress == assetAddress) {
179+
return i;
180+
}
181+
}
182+
183+
revert AssetNotFound();
184+
}
185+
186+
function isAssetInSwapOperator(uint _assetId, uint _assetsInSwapOperatorBitmap) internal pure returns (bool) {
187+
188+
if (
189+
// there are assets in the swap operator
190+
_assetsInSwapOperatorBitmap != 0 &&
191+
// asset id is not in the swap operator assets
192+
((1 << _assetId) & _assetsInSwapOperatorBitmap == 0)
193+
) {
194+
return false;
195+
}
196+
197+
return true;
198+
}
199+
159200
/* ========== ASSET RELATED MUTATIVE FUNCTIONS ========== */
160201

161202
function addAsset(
@@ -232,7 +273,7 @@ contract Pool is IPool, MasterAwareV2, ReentrancyGuard {
232273
return;
233274
}
234275

235-
revert("Pool: Asset not found");
276+
revert AssetNotFound();
236277
}
237278

238279
function transferAsset(
@@ -275,8 +316,20 @@ contract Pool is IPool, MasterAwareV2, ReentrancyGuard {
275316
swapDetails[assetAddress].lastSwapTime = lastSwapTime;
276317
}
277318

278-
function setSwapValue(uint newValue) external onlySwapOperator whenNotPaused {
279-
swapValue = newValue.toUint96();
319+
function setSwapAssetAmount(address assetAddress, uint value) external onlySwapOperator whenNotPaused {
320+
321+
uint assetId = getAssetId(assetAddress);
322+
assetInSwapOperator = value;
323+
324+
if (value > 0) {
325+
if (assetsInSwapOperatorBitmap != 0) {
326+
revert OrderInProgress();
327+
}
328+
329+
assetsInSwapOperatorBitmap = uint32(1 << assetId);
330+
} else {
331+
assetsInSwapOperatorBitmap = 0;
332+
}
280333
}
281334

282335
/* ========== CLAIMS RELATED MUTATIVE FUNCTIONS ========== */
@@ -429,7 +482,7 @@ contract Pool is IPool, MasterAwareV2, ReentrancyGuard {
429482
}
430483

431484
function updateUintParameters(bytes8 /* code */, uint /* value */) external view onlyGovernance {
432-
revert("Pool: Unknown parameter");
485+
revert UnknownParameter();
433486
}
434487

435488
function updateAddressParameters(bytes8 code, address value) external onlyGovernance {
@@ -447,7 +500,7 @@ contract Pool is IPool, MasterAwareV2, ReentrancyGuard {
447500
return;
448501
}
449502

450-
revert("Pool: Unknown parameter");
503+
revert UnknownParameter();
451504
}
452505

453506
/* ========== DEPENDENCIES ========== */

contracts/modules/capital/SwapOperator.sol

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ contract SwapOperator is ISwapOperator {
4848
address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
4949
uint public constant MAX_SLIPPAGE_DENOMINATOR = 10000;
5050
uint public constant MIN_VALID_TO_PERIOD = 600; // 10 minutes
51-
uint public constant MAX_VALID_TO_PERIOD = 3600; // 60 minutes
51+
uint public constant MAX_VALID_TO_PERIOD = 31 days; // 1 month
5252
uint public constant MIN_TIME_BETWEEN_ORDERS = 900; // 15 minutes
5353
uint public constant MAX_FEE = 0.3 ether;
5454

@@ -284,38 +284,37 @@ contract SwapOperator is ISwapOperator {
284284
/// Additionally if selling ETH, wraps received Pool ETH to WETH
285285
function executeAssetTransfer(
286286
IPool pool,
287-
IPriceFeedOracle priceFeedOracle,
288287
GPv2Order.Data calldata order,
289288
SwapOperationType swapOperationType,
290289
uint totalOutAmount
291-
) internal returns (uint swapValueEth) {
290+
) internal {
292291
address sellTokenAddress = address(order.sellToken);
293292
address buyTokenAddress = address(order.buyToken);
294293

295294
if (swapOperationType == SwapOperationType.EthToAsset) {
296295
// set lastSwapTime of buyToken only (sellToken WETH has no set swapDetails)
297296
pool.setSwapDetailsLastSwapTime(buyTokenAddress, uint32(block.timestamp));
297+
// Set the setSwapAssetAmount on the pool
298+
pool.setSwapAssetAmount(ETH, totalOutAmount);
298299
// transfer ETH from pool and wrap it (use ETH address here because swapOp.sellToken is WETH address)
299300
pool.transferAssetToSwapOperator(ETH, totalOutAmount);
300301
weth.deposit{value: totalOutAmount}();
301-
// no need to convert since totalOutAmount is already in ETH (i.e. WETH)
302-
swapValueEth = totalOutAmount;
303302
} else if (swapOperationType == SwapOperationType.AssetToEth) {
304303
// set lastSwapTime of sellToken only (buyToken WETH has no set swapDetails)
305304
pool.setSwapDetailsLastSwapTime(sellTokenAddress, uint32(block.timestamp));
305+
// Set the setSwapAssetAmount on the pool
306+
pool.setSwapAssetAmount(sellTokenAddress, totalOutAmount);
306307
// transfer ERC20 asset from Pool
307308
pool.transferAssetToSwapOperator(sellTokenAddress, totalOutAmount);
308-
// convert totalOutAmount (sellAmount + fee) to ETH
309-
swapValueEth = priceFeedOracle.getEthForAsset(sellTokenAddress, totalOutAmount);
310309
} else {
311310
// SwapOperationType.AssetToAsset
312311
// set lastSwapTime of sell / buy tokens
313312
pool.setSwapDetailsLastSwapTime(sellTokenAddress, uint32(block.timestamp));
314313
pool.setSwapDetailsLastSwapTime(buyTokenAddress, uint32(block.timestamp));
314+
// Set the setSwapAssetAmount on the pool
315+
pool.setSwapAssetAmount(sellTokenAddress, totalOutAmount);
315316
// transfer ERC20 asset from Pool
316317
pool.transferAssetToSwapOperator(sellTokenAddress, totalOutAmount);
317-
// convert totalOutAmount (sellAmount + fee) to ETH
318-
swapValueEth = priceFeedOracle.getEthForAsset(sellTokenAddress, totalOutAmount);
319318
}
320319
}
321320

@@ -343,10 +342,7 @@ contract SwapOperator is ISwapOperator {
343342
performPreSwapValidations(pool, priceFeedOracle, order, swapOperationType, totalOutAmount);
344343

345344
// Execute swap based on operation type
346-
uint swapValueEth = executeAssetTransfer(pool, priceFeedOracle, order, swapOperationType, totalOutAmount);
347-
348-
// Set the swapValue on the pool
349-
pool.setSwapValue(swapValueEth);
345+
executeAssetTransfer(pool, order, swapOperationType, totalOutAmount);
350346

351347
// Approve cowVaultRelayer contract to spend sellToken totalOutAmount
352348
order.sellToken.safeApprove(cowVaultRelayer, totalOutAmount);
@@ -394,8 +390,9 @@ contract SwapOperator is ISwapOperator {
394390
returnAssetToPool(pool, order.buyToken);
395391
returnAssetToPool(pool, order.sellToken);
396392

393+
address sellToken = address(order.sellToken) == address(weth) ? ETH : address(order.sellToken);
397394
// Set swapValue on pool to 0
398-
pool.setSwapValue(0);
395+
pool.setSwapAssetAmount(sellToken, 0);
399396

400397
// Emit event
401398
emit OrderClosed(order, filledAmount);

deployments/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deployments/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nexusmutual/deployments",
3-
"version": "2.8.0",
3+
"version": "2.9.0",
44
"description": "Nexus Mutual deployed contract addresses and abis",
55
"typings": "./dist/index.d.ts",
66
"main": "./dist/index.js",

0 commit comments

Comments
 (0)