• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RigoBlock / v3-contracts / 13261352678

11 Feb 2025 10:59AM UTC coverage: 84.94% (+2.6%) from 82.368%
13261352678

Pull #622

github

web-flow
Merge f22d260e3 into 08bd3b51b
Pull Request #622: feat: automated nav

761 of 962 branches covered (79.11%)

Branch coverage included in aggregate %.

523 of 711 new or added lines in 28 files covered. (73.56%)

18 existing lines in 5 files now uncovered.

1698 of 1933 relevant lines covered (87.84%)

44.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.69
/contracts/protocol/extensions/adapters/AUniswapRouter.sol
1
// SPDX-License-Identifier: Apache-2.0-or-later
2
/*
3

4
 Copyright 2025 Rigo Intl.
5

6
 Licensed under the Apache License, Version 2.0 (the "License");
7
 you may not use this file except in compliance with the License.
8
 You may obtain a copy of the License at
9

10
     http://www.apache.org/licenses/LICENSE-2.0
11

12
 Unless required by applicable law or agreed to in writing, software
13
 distributed under the License is distributed on an "AS IS" BASIS,
14
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 See the License for the specific language governing permissions and
16
 limitations under the License.
17

18
*/
19

20
// solhint-disable-next-line
21
pragma solidity 0.8.28;
22

23
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
24
import {BaseHook} from "@uniswap/v4-periphery/src/base/hooks/BaseHook.sol";
25
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
26
import {CalldataDecoder} from "@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol";
27
import {IERC20} from "../../interfaces/IERC20.sol";
28
import {ApplicationsLib, ApplicationsSlot} from "../../libraries/ApplicationsLib.sol";
29
import {EnumerableSet, AddressSet, Pool} from "../../libraries/EnumerableSet.sol";
30
import {SafeTransferLib} from "../../libraries/SafeTransferLib.sol";
31
import {SlotDerivation} from "../../libraries/SlotDerivation.sol";
32
import {StorageLib} from "../../libraries/StorageLib.sol";
33
import {TransientSlot} from "../../libraries/TransientSlot.sol";
34
import {Applications, TokenIdsSlot} from "../../types/Applications.sol";
35
import {IAUniswapRouter, IPositionManager} from "./interfaces/IAUniswapRouter.sol";
36
import {IEOracle} from "./interfaces/IEOracle.sol";
37
import {AUniswapDecoder} from "./AUniswapDecoder.sol";
38

39
interface IERC721 {
40
    function ownerOf(uint256 id) external view returns (address);
41
    function balanceOf(address owner) external view returns (uint256);
42
}
43

44
interface IUniswapRouter {
45
    function execute(bytes calldata commands, bytes[] calldata inputs) external payable;
46
}
47

48
/// @title AUniswapRouter - Allows interactions with the Uniswap universal router contracts.
49
/// @notice This contract is used as a bridge between a Rigoblock smart pool contract and the Uniswap universal router.
50
/// @dev This contract ensures that tokens approvals are set and removed correctly, and that recipient and tokens are validated.
51
/// @author Gabriele Rigo - <gab@rigoblock.com>
52
contract AUniswapRouter is IAUniswapRouter, AUniswapDecoder {
53
    type Uint256Slot is bytes32;
54
    type AddressSlot is bytes32;
55
    type BooleanSlot is bytes32;
56

57
    using CalldataDecoder for bytes;
58
    using TransientSlot for *;
59
    using SlotDerivation for bytes32;
60
    using ApplicationsLib for ApplicationsSlot;
61
    using EnumerableSet for AddressSet;
62
    using SafeTransferLib for address;
63

64
    /// @notice Thrown when executing commands with an expired deadline
65
    error TransactionDeadlinePassed();
66
    error PositionOwner();
67
    error RecipientIsNotSmartPool();
68
    error ReentrantCall();
69
    error NestedSubPlan();
70
    error UniV4PositionsLimitExceeded();
71

72
    string public constant override requiredVersion = "4.0.0";
73

74
    // transient storage slots, only used by this contract
75
    // bytes32(uint256(keccak256("AUniswapRouter.lock")) - 1)
76
    bytes32 private constant _LOCK_SLOT = 0x1e2a0e74e761035cb113c1bf11b7fbac06ae91f3a03ce360dda726ba116c216f;
77
    // bytes32(uint256(keccak256("AUniswapRouter.reentrancy.depth")) - 1)
78
    bytes32 private constant _REENTRANCY_DEPTH_SLOT =
79
        0x3921e0fb5d7436d70b7041cccb0d0f543e6b643f41e09aa71450d5e1c5767376;
80

81
    // TODO: can import?
82
    uint256 private constant NIL_VALUE = 0;
83

84
    // TODO: check store as inintiate instances
85
    address private immutable _uniswapRouter;
86
    IPositionManager private immutable _positionManager;
87

88
    // TODO: should verify that it is ok to make direct calls, as they could potentially modify state of the adapter
89
    // either we make sure that a constructor value prevents setting, or we require delegatecall, which would prevent
90
    // view methods from msg.sender other than the pool operator.
91
    constructor(address _universalRouter, address _v4Posm, address weth) AUniswapDecoder(weth) {
92
        _uniswapRouter = _universalRouter;
3✔
93
        _positionManager = IPositionManager(_v4Posm);
3✔
94
    }
95

96
    modifier checkDeadline(uint256 deadline) {
UNCOV
97
        require(block.timestamp <= deadline, TransactionDeadlinePassed());
×
NEW
98
        _;
×
99
    }
100

101
    modifier nonReentrant() {
UNCOV
102
        if (!_lockSlot().asBoolean().tload()) {
×
NEW
103
            _lockSlot().asBoolean().tstore(true);
×
104
        } else {
NEW
105
            require(msg.sender == address(this), ReentrantCall());
×
106
        }
NEW
107
        _reentrancyDepthSlot().asUint256().tstore(_reentrancyDepthSlot().asUint256().tload() + 1);
×
UNCOV
108
        _;
×
UNCOV
109
        _reentrancyDepthSlot().asUint256().tstore(_reentrancyDepthSlot().asUint256().tload() - 1);
×
UNCOV
110
        if (_reentrancyDepthSlot().asUint256().tload() == 0) {
×
NEW
111
            _lockSlot().asBoolean().tstore(false);
×
112
        }
113
    }
114

115
    function _lockSlot() private pure returns (bytes32) {
NEW
116
        return _LOCK_SLOT;
×
117
    }
118

119
    function _reentrancyDepthSlot() private pure returns (bytes32) {
NEW
120
        return _REENTRANCY_DEPTH_SLOT;
×
121
    }
122

123
    /// @inheritdoc IAUniswapRouter
124
    function execute(
125
        bytes calldata commands,
126
        bytes[] calldata inputs,
127
        uint256 deadline
128
    ) external override checkDeadline(deadline) returns (Parameters memory params) {
×
NEW
129
        return execute(commands, inputs);
×
130
    }
131

132
    /// @inheritdoc IAUniswapRouter
133
    function execute(
134
        bytes calldata commands,
135
        bytes[] calldata inputs
136
    ) public override nonReentrant returns (Parameters memory params) {
×
NEW
137
        assert(commands.length == inputs.length);
×
138

139
        // loop through all given commands, verify their inputs and pass along outputs as defined
UNCOV
140
        for (uint256 i = 0; i < commands.length; i++) {
×
141
            // input sanity check and parameters return
NEW
142
            params = _decodeInput(commands[i], inputs[i], params);
×
143
        }
144

145
        // only execute when finished decoding inputs
NEW
146
        if (_reentrancyDepthSlot().asUint256().tload() == 1) {
×
147
            // early return if recipient is not the caller
UNCOV
148
            _processRecipients(params.recipients);
×
149

UNCOV
150
            _assertTokensOutHavePriceFeed(params.tokensOut);
×
151

152
            // we approve all the tokens that are exiting the smart pool
NEW
153
            _safeApproveTokensIn(params.tokensIn, uniswapRouter(), type(uint256).max);
×
154

155
            // forward the inputs to the Uniswap universal router
156
            try IUniswapRouter(uniswapRouter()).execute{value: params.value}(commands, inputs) {
×
157
                // we remove allowance without clearing storage
UNCOV
158
                _safeApproveTokensIn(params.tokensIn, uniswapRouter(), 1);
×
NEW
159
                return params;
×
160
            } catch Error(string memory reason) {
UNCOV
161
                revert(reason);
×
162
            }
163
        }
164
    }
165

166
    // TODO: add non-reentrant modifier (can be used as can only be reentered by itself, which it won't)
167
    /// @inheritdoc IAUniswapRouter
168
    /// @notice Can be not reentrancy-protected, as will revert in PositionManager
169
    function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external override {
170
        (bytes calldata actions, bytes[] calldata params) = unlockData.decodeActionsRouterParams();
9✔
171
        assert(actions.length == params.length);
9✔
172
        Parameters memory newParams;
9✔
173
        Position[] memory positions;
9✔
174

175
        for (uint256 actionIndex = 0; actionIndex < actions.length; actionIndex++) {
9✔
176
            (newParams, positions) =
13✔
177
                _decodePosmAction(uint8(actions[actionIndex]), params[actionIndex], newParams, positions);
178
        }
179

180
        _processRecipients(newParams.recipients);
9✔
181
        _assertTokensOutHavePriceFeed(newParams.tokensOut);
8✔
182
        _safeApproveTokensIn(newParams.tokensIn, address(uniV4Posm()), type(uint256).max);
8✔
183

184
        try uniV4Posm().modifyLiquidities{value: newParams.value}(unlockData, deadline) {
8✔
185
            _safeApproveTokensIn(newParams.tokensIn, address(uniV4Posm()), 1);
8✔
186
            _processTokenIds(positions);
8✔
187
            return;
7✔
188
        } catch Error(string memory reason) {
UNCOV
189
            revert(reason);
×
190
        }
191
    }
192

193
    /// @inheritdoc IAUniswapRouter
194
    function uniV4Posm() public view override(IAUniswapRouter, AUniswapDecoder) returns (IPositionManager) {
195
        return _positionManager;
30✔
196
    }
197

198
    /// @inheritdoc IAUniswapRouter
199
    function uniswapRouter() public view override returns (address universalRouter) {
NEW
200
        return _uniswapRouter;
×
201
    }
202

203
    /// @notice An implementation before v4 will be rejected here
204
    function _assertTokensOutHavePriceFeed(address[] memory tokensOut) private {
205
        // load active tokens from storage
206
        AddressSet storage values = StorageLib.activeTokensSet();
8✔
207

208
        for (uint256 i = 0; i < tokensOut.length; i++) {
8✔
209
            // update storage with new token
210
            values.addUnique(IEOracle(address(this)), tokensOut[i], StorageLib.pool().baseToken);
10✔
211
        }
212
    }
213

214
    function _safeApproveTokensIn(address[] memory tokensIn, address spender, uint256 amount) private {
215
        for (uint256 i = 0; i < tokensIn.length; i++) {
16✔
216
            // cannot approve base currency, early return
NEW
217
            if (tokensIn[i].isAddressZero()) {
×
218
                continue;
×
219
            }
220

NEW
221
            tokensIn[i].safeApprove(spender, amount);
×
222

223
            // assert no approval inflation exists after removing approval
UNCOV
224
            if (amount == 1) {
×
UNCOV
225
                assert(IERC20(tokensIn[i]).allowance(address(this), uniswapRouter()) == 1);
×
226
            }
227
        }
228
    }
229

230
    /// @dev This is executed after the uniswap Posm deltas have been settled.
231
    function _processTokenIds(Position[] memory positions) private {
232
        // do not load values unless we are writing to storage
233
        if (positions.length > 0) {
8!
234
            // update tokenIds in proxy persistent storage.
235
            TokenIdsSlot storage idsSlot = StorageLib.uniV4TokenIdsSlot();
8✔
236

237
            for (uint256 i = 0; i < positions.length; i++) {
8✔
238
                if (positions[i].action == Actions.MINT_POSITION) {
12✔
239
                    // Assert hook does not have access to deltas. Hook address is returned for mint ops only.
240
                    // If moving the following block to protect all actions, make sure hook address is appended.
241
                    if (positions[i].hook != ZERO_ADDRESS) {
5!
NEW
242
                        Hooks.Permissions memory permissions = BaseHook(positions[i].hook).getHookPermissions();
×
243

244
                        // we prevent hooks to that can access pool liquidity
NEW
245
                        require(
×
246
                            !permissions.afterAddLiquidityReturnDelta && !permissions.afterRemoveLiquidityReturnDelta,
247
                            LiquidityMintHookError(positions[i].hook)
248
                        );
249
                    }
250

251
                    // mint reverts if tokenId exists, so we can be sure it is unique
252
                    uint256 storedLength = idsSlot.tokenIds.length;
5✔
253
                    require(storedLength < 255, UniV4PositionsLimitExceeded());
5!
254
            
255
                    // position 0 is flag for removed
256
                    idsSlot.positions[positions[i].tokenId] = ++storedLength;
5✔
257
                    idsSlot.tokenIds.push(positions[i].tokenId);
5✔
258
                    continue;
5✔
259
                } else {
260
                    // position must be active in pool storage. This means pool cannot modify liquidity created on its behalf.
261
                    // This is helpful for position retrieval for nav calculations, otherwise we'd have to push it to storage.
262
                    // If we remove this assertion, we must make sure that the non-nil hook address is appended, as it would
263
                    // allow action on a potentially malicious hook minted to the pool, and we must make sure pool is position owner.
264
                    require(idsSlot.positions[positions[i].tokenId] != 0, PositionOwner());
7✔
265

266
                    if (positions[i].action == Actions.BURN_POSITION) {
6✔
267
                        idsSlot.positions[positions[i].tokenId] = 0;
1✔
268
                        idsSlot.tokenIds.pop();
1✔
269
                        continue;
1✔
270
                    }
271
                }
272
            }
273

274
            // activate/remove application in proxy persistent storage.
275
            uint256 appsBitmap = StorageLib.activeApplications().packedApplications;
7✔
276
            uint256 appFlag = uint256(Applications.UNIV4_LIQUIDITY);
7✔
277
            bool isActiveApp = ApplicationsLib.isActiveApplication(appsBitmap, appFlag);
7✔
278

279
            // we update application status after all tokenIds have been processed
280
            if (StorageLib.uniV4TokenIdsSlot().tokenIds.length > 0) {
7✔
281
                if (!isActiveApp) {
6✔
282
                    // activate uniV4 liquidity application
283
                    StorageLib.activeApplications().storeApplication(appFlag);
5✔
284
                }
285
            } else {
286
                if (isActiveApp) {
1!
287
                    // remove uniV4 liquidity application
288
                    StorageLib.activeApplications().removeApplication(appFlag);
1✔
289
                }
290
            }
291
        }
292
    }
293

294
    function _processRecipients(address[] memory recipients) private view {
295
        for (uint256 i = 0; i < recipients.length; i++) {
9✔
296
            require(recipients[i] == address(this), RecipientIsNotSmartPool());
6✔
297
        }
298
    }
299
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc