Skip to content

Commit fd26a14

Browse files
authored
Merge pull request #303 from consenlabs/leave-one-wei
gas optimization: leave one wei feature
2 parents 3018296 + d1e2f50 commit fd26a14

File tree

6 files changed

+126
-23
lines changed

6 files changed

+126
-23
lines changed

contracts/GenericSwap.sol

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,19 @@ contract GenericSwap is IGenericSwap, TokenCollector, EIP712 {
6767

6868
if (_inputToken.isETH()) {
6969
if (msg.value != _swapData.takerTokenAmount) revert InvalidMsgValue();
70-
}
71-
72-
if (!_inputToken.isETH()) {
70+
} else {
7371
if (msg.value != 0) revert InvalidMsgValue();
7472
_collect(_inputToken, _authorizedUser, _swapData.maker, _swapData.takerTokenAmount, _takerTokenPermit);
7573
}
7674

7775
IStrategy(_swapData.maker).executeStrategy{ value: msg.value }(_inputToken, _outputToken, _swapData.takerTokenAmount, _swapData.strategyData);
7876

7977
returnAmount = _outputToken.getBalance(address(this));
78+
if (returnAmount > 1) {
79+
unchecked {
80+
--returnAmount;
81+
}
82+
}
8083
if (returnAmount < _swapData.minMakerTokenAmount) revert InsufficientOutput();
8184

8285
_outputToken.transferTo(_swapData.recipient, returnAmount);

contracts/SmartOrderStrategy.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ contract SmartOrderStrategy is ISmartOrderStrategy, AdminManagement {
5757
}
5858
}
5959
uint256 selfBalance = Asset.getBalance(outputToken, address(this));
60+
if (selfBalance > 1) {
61+
unchecked {
62+
--selfBalance;
63+
}
64+
}
6065
Asset.transferTo(outputToken, payable(genericSwap), selfBalance);
6166
}
6267

@@ -66,6 +71,12 @@ contract SmartOrderStrategy is ISmartOrderStrategy, AdminManagement {
6671
// replace amount if ratio != 0
6772
if (_inputRatio != 0) {
6873
uint256 inputTokenBalance = IERC20(_inputToken).balanceOf(address(this));
74+
// leaving one wei for gas optimization
75+
if (inputTokenBalance > 1) {
76+
unchecked {
77+
--inputTokenBalance;
78+
}
79+
}
6980

7081
// calculate input amount if ratio should be applied
7182
if (_inputRatio != Constant.BPS_MAX) {

contracts/libraries/GenericSwapData.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct GenericSwapData {
2121
}
2222

2323
// solhint-disable-next-line func-visibility
24+
// free functions cannot have function visibility
2425
function getGSDataHash(GenericSwapData memory gsData) pure returns (bytes32) {
2526
return
2627
keccak256(

test/forkMainnet/GenericSwap.t.sol

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,21 @@ contract GenericSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper
3838
address strategyAdmin = makeAddr("strategyAdmin");
3939
address allowanceTargetOwner = makeAddr("allowanceTargetOwner");
4040
uint256 takerPrivateKey = uint256(1);
41+
uint256 alicePrivateKey = uint256(2);
4142
address taker = vm.addr(takerPrivateKey);
43+
address alice = vm.addr(alicePrivateKey);
4244
uint256 defaultExpiry = block.timestamp + 1;
4345
address defaultInputToken = USDT_ADDRESS;
4446
uint256 defaultInputAmount = 10 * 1e6;
4547
address defaultOutputToken = DAI_ADDRESS;
4648
address[] defaultPath = [defaultInputToken, defaultOutputToken];
4749
uint24[] defaultV3Fees = [3000];
4850
bytes defaultTakerPermit;
51+
bytes alicePermit;
4952
SmartOrderStrategy smartStrategy;
5053
GenericSwap genericSwap;
5154
GenericSwapData defaultGSData;
55+
GenericSwapData aliceGSData;
5256
MockStrategy mockStrategy;
5357
AllowanceTarget allowanceTarget;
5458

@@ -62,6 +66,7 @@ contract GenericSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper
6266

6367
genericSwap = new GenericSwap(UNISWAP_PERMIT2_ADDRESS, address(allowanceTarget));
6468
smartStrategy = new SmartOrderStrategy(strategyAdmin, address(genericSwap), WETH_ADDRESS);
69+
6570
mockStrategy = new MockStrategy();
6671
vm.prank(strategyAdmin);
6772
address[] memory tokenList = new address[](1);
@@ -72,7 +77,7 @@ contract GenericSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper
7277

7378
IUniswapV3Quoter v3Quoter = IUniswapV3Quoter(UNISWAP_V3_QUOTER_ADDRESS);
7479
bytes memory encodedPath = UniswapV3.encodePath(defaultPath, defaultV3Fees);
75-
uint256 expectedOut = v3Quoter.quoteExactInput(encodedPath, defaultInputAmount);
80+
uint256 expectedOut = v3Quoter.quoteExactInput(encodedPath, defaultInputAmount) - 2; // leaving 1 wei in GS and SOS separately
7681
uint256 minOutputAmount = (expectedOut * 95) / 100; // default 5% slippage tolerance
7782
bytes memory routerPayload = abi.encodeCall(
7883
IUniswapSwapRouter02.exactInputSingle,
@@ -101,6 +106,8 @@ contract GenericSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper
101106

102107
deal(taker, 100 ether);
103108
setTokenBalanceAndApprove(taker, UNISWAP_PERMIT2_ADDRESS, tokens, 100000);
109+
deal(alice, 100 ether);
110+
setTokenBalanceAndApprove(alice, UNISWAP_PERMIT2_ADDRESS, tokens, 100000);
104111
deal(address(mockStrategy), 100 ether);
105112
setTokenBalanceAndApprove(address(mockStrategy), UNISWAP_PERMIT2_ADDRESS, tokens, 100000);
106113

@@ -156,16 +163,17 @@ contract GenericSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper
156163
Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: address(mockStrategy), token: gsData.makerToken });
157164

158165
uint256 actualOutput = 900;
166+
uint256 realChangedInGS = actualOutput - 1; // leaving 1 wei in GS
159167

160168
// 800 < 900 < 1000
161169
mockStrategy.setOutputAmountAndRecipient(actualOutput, payable(address(genericSwap)));
162170
vm.expectEmit(true, true, true, true);
163-
emit Swap(getGSDataHash(gsData), gsData.maker, taker, taker, gsData.takerToken, gsData.takerTokenAmount, gsData.makerToken, actualOutput);
171+
emit Swap(getGSDataHash(gsData), gsData.maker, taker, taker, gsData.takerToken, gsData.takerTokenAmount, gsData.makerToken, realChangedInGS);
164172
vm.prank(taker);
165173
genericSwap.executeSwap(gsData, defaultTakerPermit);
166174

167175
takerTakerToken.assertChange(-int256(gsData.takerTokenAmount));
168-
takerMakerToken.assertChange(int256(actualOutput));
176+
takerMakerToken.assertChange(int256(realChangedInGS));
169177
makerTakerToken.assertChange(int256(gsData.takerTokenAmount));
170178
makerMakerToken.assertChange(-int256(actualOutput));
171179
}
@@ -176,19 +184,21 @@ contract GenericSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper
176184
gsData.takerToken = Constant.ETH_ADDRESS;
177185
gsData.takerTokenAmount = 1 ether;
178186

187+
uint256 realChangedInGS = gsData.makerTokenAmount - 1; // leaving 1 wei in GS
188+
179189
Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: gsData.takerToken });
180190
Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: gsData.makerToken });
181191
Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: address(mockStrategy), token: gsData.takerToken });
182192
Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: address(mockStrategy), token: gsData.makerToken });
183193

184194
mockStrategy.setOutputAmountAndRecipient(gsData.makerTokenAmount, payable(address(genericSwap)));
185195
vm.expectEmit(true, true, true, true);
186-
emit Swap(getGSDataHash(gsData), gsData.maker, taker, taker, gsData.takerToken, gsData.takerTokenAmount, gsData.makerToken, gsData.makerTokenAmount);
196+
emit Swap(getGSDataHash(gsData), gsData.maker, taker, taker, gsData.takerToken, gsData.takerTokenAmount, gsData.makerToken, realChangedInGS);
187197
vm.prank(taker);
188198
genericSwap.executeSwap{ value: gsData.takerTokenAmount }(gsData, defaultTakerPermit);
189199

190200
takerTakerToken.assertChange(-int256(gsData.takerTokenAmount));
191-
takerMakerToken.assertChange(int256(gsData.makerTokenAmount));
201+
takerMakerToken.assertChange(int256(realChangedInGS));
192202
makerTakerToken.assertChange(int256(gsData.takerTokenAmount));
193203
makerMakerToken.assertChange(-int256(gsData.makerTokenAmount));
194204
}
@@ -200,19 +210,21 @@ contract GenericSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper
200210
gsData.makerTokenAmount = 1 ether;
201211
gsData.minMakerTokenAmount = 1 ether - 1000;
202212

213+
uint256 realChangedInGS = gsData.makerTokenAmount - 1; // leaving 1 wei in GS
214+
203215
Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: gsData.takerToken });
204216
Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: gsData.makerToken });
205217
Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: address(mockStrategy), token: gsData.takerToken });
206218
Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: address(mockStrategy), token: gsData.makerToken });
207219

208220
mockStrategy.setOutputAmountAndRecipient(gsData.makerTokenAmount, payable(address(genericSwap)));
209221
vm.expectEmit(true, true, true, true);
210-
emit Swap(getGSDataHash(gsData), gsData.maker, taker, taker, gsData.takerToken, gsData.takerTokenAmount, gsData.makerToken, gsData.makerTokenAmount);
222+
emit Swap(getGSDataHash(gsData), gsData.maker, taker, taker, gsData.takerToken, gsData.takerTokenAmount, gsData.makerToken, realChangedInGS);
211223
vm.prank(taker);
212224
genericSwap.executeSwap(gsData, defaultTakerPermit);
213225

214226
takerTakerToken.assertChange(-int256(gsData.takerTokenAmount));
215-
takerMakerToken.assertChange(int256(gsData.makerTokenAmount));
227+
takerMakerToken.assertChange(int256(realChangedInGS));
216228
makerTakerToken.assertChange(int256(gsData.takerTokenAmount));
217229
makerMakerToken.assertChange(-int256(gsData.makerTokenAmount));
218230
}
@@ -306,4 +318,73 @@ contract GenericSwapTest is Test, Tokens, BalanceUtil, Permit2Helper, SigHelper
306318
vm.expectRevert(IGenericSwap.AlreadyFilled.selector);
307319
genericSwap.executeSwapWithSig(defaultGSData, defaultTakerPermit, taker, takerSig);
308320
}
321+
322+
function testLeaveOneWeiWithMultipleUsers() public {
323+
Snapshot memory takerTakerToken = BalanceSnapshot.take({ owner: taker, token: defaultGSData.takerToken });
324+
Snapshot memory takerMakerToken = BalanceSnapshot.take({ owner: taker, token: defaultGSData.makerToken });
325+
Snapshot memory gsTakerToken = BalanceSnapshot.take({ owner: address(genericSwap), token: defaultGSData.takerToken });
326+
Snapshot memory gsMakerToken = BalanceSnapshot.take({ owner: address(genericSwap), token: defaultGSData.makerToken });
327+
Snapshot memory makerTakerToken = BalanceSnapshot.take({ owner: defaultGSData.maker, token: defaultGSData.takerToken });
328+
Snapshot memory makerMakerToken = BalanceSnapshot.take({ owner: defaultGSData.maker, token: defaultGSData.makerToken });
329+
330+
// the first user: taker
331+
// his makerTokenAmount has already been reduced by 2 in the setup function
332+
// leaving 1 wei in GS and SOS separately
333+
vm.expectEmit(true, true, true, true);
334+
emit Swap(
335+
getGSDataHash(defaultGSData),
336+
defaultGSData.maker,
337+
taker,
338+
taker,
339+
defaultGSData.takerToken,
340+
defaultGSData.takerTokenAmount,
341+
defaultGSData.makerToken,
342+
defaultGSData.makerTokenAmount
343+
);
344+
345+
vm.prank(taker);
346+
genericSwap.executeSwap(defaultGSData, defaultTakerPermit);
347+
348+
// the second user: Alice
349+
// his makerTokenAmount is recalculate by `quoteExactInput() function base on the current state`
350+
// but there is no need to reduce it by 2 this time
351+
aliceGSData = defaultGSData;
352+
353+
IUniswapV3Quoter v3Quoter = IUniswapV3Quoter(UNISWAP_V3_QUOTER_ADDRESS);
354+
bytes memory encodedPath = UniswapV3.encodePath(defaultPath, defaultV3Fees);
355+
uint256 aliceExpectedOut = v3Quoter.quoteExactInput(encodedPath, defaultInputAmount);
356+
357+
aliceGSData.recipient = payable(alice);
358+
aliceGSData.makerTokenAmount = aliceExpectedOut;
359+
alicePermit = getTokenlonPermit2Data(alice, alicePrivateKey, aliceGSData.takerToken, address(genericSwap));
360+
361+
Snapshot memory aliceTakerToken = BalanceSnapshot.take({ owner: alice, token: aliceGSData.takerToken });
362+
Snapshot memory aliceMakerToken = BalanceSnapshot.take({ owner: alice, token: aliceGSData.makerToken });
363+
364+
vm.expectEmit(true, true, true, true);
365+
366+
emit Swap(
367+
getGSDataHash(aliceGSData),
368+
aliceGSData.maker,
369+
alice,
370+
alice,
371+
aliceGSData.takerToken,
372+
aliceGSData.takerTokenAmount,
373+
aliceGSData.makerToken,
374+
aliceGSData.makerTokenAmount
375+
);
376+
377+
vm.startPrank(alice);
378+
genericSwap.executeSwap(aliceGSData, alicePermit);
379+
vm.stopPrank();
380+
381+
takerTakerToken.assertChange(-int256(defaultGSData.takerTokenAmount));
382+
takerMakerToken.assertChange(int256(defaultGSData.makerTokenAmount));
383+
aliceTakerToken.assertChange(-int256(aliceGSData.takerTokenAmount));
384+
aliceMakerToken.assertChange(int256(aliceGSData.makerTokenAmount));
385+
gsTakerToken.assertChange(0);
386+
gsMakerToken.assertChange(1);
387+
makerTakerToken.assertChange(0);
388+
makerMakerToken.assertChange(1);
389+
}
309390
}

test/forkMainnet/SmartOrderStrategy/AMMs.t.sol

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ contract AMMsTest is SmartOrderStrategyTest {
4646

4747
// get the exact quote from uniswap
4848
uint256 expectedOut = v3Quoter.quoteExactInput(encodedUniv3Path, defaultInputAmount);
49+
uint256 realChangedInGS = expectedOut - 1;
4950

5051
vm.startPrank(genericSwap, genericSwap);
5152
IERC20(defaultInputToken).safeTransfer(address(smartOrderStrategy), defaultInputAmount);
@@ -55,7 +56,7 @@ contract AMMsTest is SmartOrderStrategyTest {
5556
vm.stopPrank();
5657

5758
sosInputToken.assertChange(-int256(defaultInputAmount));
58-
gsOutputToken.assertChange(int256(expectedOut));
59+
gsOutputToken.assertChange(int256(realChangedInGS));
5960
}
6061

6162
function testUniswapV3WithAmountReplace() public {
@@ -85,18 +86,16 @@ contract AMMsTest is SmartOrderStrategyTest {
8586
bytes memory data = abi.encode(operations);
8687

8788
// get the exact quote from uniswap
88-
uint256 inputAmountAfterRatio = (defaultInputAmount * defaultInputRatio) / Constant.BPS_MAX;
89+
uint256 inputAmountAfterRatio = ((defaultInputAmount - 1) * defaultInputRatio) / Constant.BPS_MAX;
8990
uint256 expectedOut = v3Quoter.quoteExactInput(encodedUniv3Path, inputAmountAfterRatio);
90-
9191
vm.startPrank(genericSwap, genericSwap);
9292
IERC20(defaultInputToken).safeTransfer(address(smartOrderStrategy), defaultInputAmount);
9393
Snapshot memory sosInputToken = BalanceSnapshot.take(address(smartOrderStrategy), defaultInputToken);
9494
Snapshot memory gsOutputToken = BalanceSnapshot.take(genericSwap, defaultOutputToken);
9595
smartOrderStrategy.executeStrategy(defaultInputToken, defaultOutputToken, defaultInputAmount, data);
9696
vm.stopPrank();
97-
9897
sosInputToken.assertChange(-int256(inputAmountAfterRatio));
99-
gsOutputToken.assertChange(int256(expectedOut));
98+
gsOutputToken.assertChange(int256(expectedOut - 1));
10099
}
101100

102101
function testUniswapV3WithMaxAmountReplace() public {
@@ -129,7 +128,7 @@ contract AMMsTest is SmartOrderStrategyTest {
129128
uint256 actualInputAmount = 5678;
130129

131130
// get the exact quote from uniswap
132-
uint256 expectedOut = v3Quoter.quoteExactInput(encodedUniv3Path, actualInputAmount);
131+
uint256 expectedOut = v3Quoter.quoteExactInput(encodedUniv3Path, actualInputAmount - 1);
133132

134133
vm.startPrank(genericSwap, genericSwap);
135134
IERC20(defaultInputToken).safeTransfer(address(smartOrderStrategy), actualInputAmount);
@@ -139,8 +138,8 @@ contract AMMsTest is SmartOrderStrategyTest {
139138
vm.stopPrank();
140139

141140
// the amount change will be the actual balance at the moment
142-
sosInputToken.assertChange(-int256(actualInputAmount));
143-
gsOutputToken.assertChange(int256(expectedOut));
141+
sosInputToken.assertChange(-int256(actualInputAmount - 1)); // leaving 1 wei in SOS
142+
gsOutputToken.assertChange(int256(expectedOut - 1)); // leaving 1 wei in GS
144143
}
145144

146145
function testUniswapV2WithWETHUnwrap() public {
@@ -171,6 +170,7 @@ contract AMMsTest is SmartOrderStrategyTest {
171170

172171
// get the exact quote from uniswap
173172
uint256 expectedOut = v3Quoter.quoteExactInput(encodedUniv3Path, defaultInputAmount);
173+
uint256 realChangedInGS = expectedOut - 1;
174174

175175
// set output token as ETH
176176
address outputToken = Constant.ETH_ADDRESS;
@@ -182,7 +182,7 @@ contract AMMsTest is SmartOrderStrategyTest {
182182
vm.stopPrank();
183183

184184
sosInputToken.assertChange(-int256(defaultInputAmount));
185-
gsOutputToken.assertChange(int256(expectedOut));
185+
gsOutputToken.assertChange(int256(realChangedInGS));
186186
}
187187

188188
function testMultipleAMMs() public {
@@ -208,10 +208,11 @@ contract AMMsTest is SmartOrderStrategyTest {
208208
)
209209
);
210210

211-
// exhange function selector : 0x5b41b908
211+
// exchange function selector : 0x5b41b908
212212
bytes memory curveData = abi.encodeWithSelector(0x5b41b908, 2, 0, uniOut, 0);
213213
ICurveFiV2 curvePool = ICurveFiV2(CURVE_TRICRYPTO2_POOL_ADDRESS);
214214
uint256 curveOut = curvePool.get_dy(2, 0, uniOut);
215+
uint256 realChangedInGS = curveOut - 1;
215216

216217
ISmartOrderStrategy.Operation[] memory operations = new ISmartOrderStrategy.Operation[](2);
217218
operations[0] = ISmartOrderStrategy.Operation({
@@ -240,6 +241,6 @@ contract AMMsTest is SmartOrderStrategyTest {
240241
vm.stopPrank();
241242

242243
sosInputToken.assertChange(-int256(defaultInputAmount));
243-
gsOutputToken.assertChange(int256(curveOut));
244+
gsOutputToken.assertChange(int256(realChangedInGS));
244245
}
245246
}

test/forkMainnet/SmartOrderStrategy/IntegrationV6.t.sol

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ contract IntegrationV6Test is SmartOrderStrategyTest, SigHelper {
6464
expiry: defaultExpiry,
6565
salt: defaultSalt
6666
});
67+
68+
uint256 realChangedInGS = rfqOffer.makerTokenAmount - 1; // leaving 1 wei in GS
69+
6770
RFQTx memory rfqTx = RFQTx({ rfqOffer: rfqOffer, takerRequestAmount: rfqOffer.takerTokenAmount, recipient: payable(address(smartOrderStrategy)) });
6871
bytes memory makerSig = signRFQOffer(makerPrivateKey, rfqOffer, address(rfq));
6972
bytes memory rfqData = abi.encodeWithSelector(RFQ_FILL_SELECTOR, rfqTx, makerSig, defaultPermit, defaultPermit);
@@ -87,7 +90,7 @@ contract IntegrationV6Test is SmartOrderStrategyTest, SigHelper {
8790
vm.stopPrank();
8891

8992
sosInputToken.assertChange(-int256(rfqOffer.takerTokenAmount));
90-
gsOutputToken.assertChange(int256(rfqOffer.makerTokenAmount));
93+
gsOutputToken.assertChange(int256(realChangedInGS));
9194
}
9295

9396
function testV6LOIntegration() public {
@@ -103,6 +106,9 @@ contract IntegrationV6Test is SmartOrderStrategyTest, SigHelper {
103106
expiry: defaultExpiry,
104107
salt: defaultSalt
105108
});
109+
110+
uint256 realChangedInGS = order.makerTokenAmount - 1; // leaving 1 wei in GS
111+
106112
bytes memory makerSig = signLimitOrder(makerPrivateKey, order, address(limitOrderSwap));
107113
ILimitOrderSwap.TakerParams memory takerParams = ILimitOrderSwap.TakerParams({
108114
takerTokenAmount: order.takerTokenAmount,
@@ -132,6 +138,6 @@ contract IntegrationV6Test is SmartOrderStrategyTest, SigHelper {
132138
vm.stopPrank();
133139

134140
sosInputToken.assertChange(-int256(order.takerTokenAmount));
135-
gsOutputToken.assertChange(int256(order.makerTokenAmount));
141+
gsOutputToken.assertChange(int256(realChangedInGS));
136142
}
137143
}

0 commit comments

Comments
 (0)