Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QA Report #263

Open
code423n4 opened this issue Jun 19, 2022 · 3 comments
Open

QA Report #263

code423n4 opened this issue Jun 19, 2022 · 3 comments
Labels
bug Something isn't working QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax

Comments

@code423n4
Copy link
Contributor

Overview

Risk Rating Number of issues
Low Risk 17
Non-Critical Risk 9

Table of Contents

Low Risk Issues

1. Check that _sponsorVault is a contract

_sponsorVault being an EOA instead of a contract can cause issues. Consider doing just like in setExecutor():

File: BridgeFacet.sol
250:   function setSponsorVault(address _sponsorVault) external onlyOwner {
251:     address old = address(s.sponsorVault);
- 252:     if (old == _sponsorVault) revert BridgeFacet__setSponsorVault_invalidSponsorVault();
+ 252:     if (old == _sponsorVault || !Address.isContract(_sponsorVault)) revert BridgeFacet__setSponsorVault_invalidSponsorVault();
253: 
254:     s.sponsorVault = ISponsorVault(_sponsorVault);
255:     emit SponsorVaultUpdated(old, _sponsorVault, msg.sender);
256:   }

2. al, fee and adminFee cannot be set the their maximum value

Consider replacing < with <= here:

File: StableSwap.sol
96:     require(_a < AmplificationUtils.MAX_A, "_a exceeds maximum");

3. Add constructor initializers

As per OpenZeppelin’s (OZ) recommendation, “The guidelines are now to make it impossible for anyone to run initialize on an implementation contract, by adding an empty constructor with the initializer modifier. So the implementation contract gets initialized automatically upon deployment.”

Note that this behaviour is also incorporated the OZ Wizard since the UUPS vulnerability discovery: “Additionally, we modified the code generated by the Wizard 19 to include a constructor that automatically initializes the implementation when deployed.”

Furthermore, this thwarts any attempts to frontrun the initialization tx of these contracts:

core/connext/helpers/BridgeToken.sol:34:  function initialize() public override initializer {
core/connext/helpers/LPToken.sol:21:  function initialize(string memory name, string memory symbol) external initializer returns (bool) {
core/connext/helpers/StableSwap.sol:61:  function initialize(
core/connext/helpers/TokenRegistry.sol:73:  function initialize(address _tokenBeacon, address _xAppConnectionManager) public initializer {
core/connext/interfaces/IBridgeToken.sol:5:  function initialize() external;
core/connext/interfaces/IStableSwap.sol:99:  function initialize(
core/promise/PromiseRouter.sol:146:  function initialize(address _xAppConnectionManager) public initializer {
core/relayer-fee/RelayerFeeRouter.sol:80:  function initialize(address _xAppConnectionManager) public initializer {

4. Unsafe casting may overflow

SafeMath and Solidity 0.8.* handles overflows for basic math operations but not for casting.
Consider using OpenZeppelin's SafeCast library to prevent unexpected overflows when casting from uint256 here:

core/connext/libraries/ConnextMessage.sol:49:    _view.assertType(uint40(_t));
core/connext/libraries/ConnextMessage.sol:71:    return actionType(_action) == uint8(_type) && messageType(_action) == _type;
core/connext/libraries/ConnextMessage.sol:138:      abi.encodePacked(Types.Transfer, _to, _amnt, _detailsHash, _transferId).ref(0).castTo(uint40(Types.Transfer));
core/connext/libraries/ConnextMessage.sol:157:    return abi.encodePacked(_domain, _id).ref(0).castTo(uint40(Types.TokenId));
core/connext/libraries/ConnextMessage.sol:188:      return _message.castTo(uint40(Types.Message));
core/connext/libraries/ConnextMessage.sol:201:    return Types(uint8(_view.typeOf()));
core/connext/libraries/ConnextMessage.sol:210:    return _message.slice(0, TOKEN_ID_LEN, uint40(Types.TokenId));
core/connext/libraries/ConnextMessage.sol:220:    uint40 _type = uint40(msgType(_message));
core/connext/libraries/ConnextMessage.sol:232:    return uint32(_tokenId.indexUint(0, 4));
core/connext/libraries/ConnextMessage.sol:263:    return uint8(_message.indexUint(TOKEN_ID_LEN, 1));
core/connext/libraries/ConnextMessage.sol:272:    return uint8(_action.indexUint(0, 1));
core/connext/libraries/Encoding.sol:37:      uint8 _b = uint8(_bytes >> (i * 8));
core/connext/libraries/Encoding.sol:46:        uint8 _b = uint8(_bytes >> (i * 8));
core/connext/libraries/Encoding.sol:62:    _char = uint8(NIBBLE_LOOKUP[_nibble]);
core/connext/libraries/LibCrossDomainProperty.sol:48:    _view.assertType(uint40(_t));
core/connext/libraries/LibCrossDomainProperty.sol:71:    return propertyType(_property) == uint8(_type);
core/connext/libraries/LibCrossDomainProperty.sol:89:    return uint8(_property.indexUint(0, 1));
core/connext/libraries/LibCrossDomainProperty.sol:99:      return _view.castTo(uint40(Types.DomainAndSender));
core/connext/libraries/LibCrossDomainProperty.sol:130:    return uint32(_property.indexUint(1, 4));
core/connext/libraries/LibCrossDomainProperty.sol:140:    return abi.encodePacked(Types.DomainAndSender, _domain, _sender).ref(0).castTo(uint40(Types.DomainAndSender));
core/connext/libraries/LibDiamond.sol:124:    uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
core/connext/libraries/LibDiamond.sol:142:    uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facetAddress].functionSelectors.length);
core/connext/libraries/LibDiamond.sol:201:      ds.selectorToFacetAndPosition[lastSelector].functionSelectorPosition = uint96(selectorPosition);
core/promise/libraries/PromiseMessage.sol:41:    _view.assertType(uint40(_t));
core/promise/libraries/PromiseMessage.sol:63:        uint8(Types.PromiseCallback),
core/promise/libraries/PromiseMessage.sol:66:        uint8(_returnSuccess ? 1 : 0),
core/promise/libraries/PromiseMessage.sol:152:      return _view.castTo(uint40(Types.PromiseCallback));
core/promise/PromiseRouter.sol:313:    return (uint64(_origin) << 32) | _nonce;
core/relayer-fee/libraries/RelayerFeeMessage.sol:44:    _view.assertType(uint40(_t));
core/relayer-fee/libraries/RelayerFeeMessage.sol:57:    return abi.encodePacked(uint8(Types.ClaimFees), _recipient, _transferIds.length, _transferIds);
core/relayer-fee/libraries/RelayerFeeMessage.sol:109:      return _view.castTo(uint40(Types.ClaimFees));
core/relayer-fee/RelayerFeeRouter.sol:166:    return (uint64(_origin) << 32) | _nonce;

5. Uninitialized Upgradeable contract

Similar issue in the past: here

Upgradeable dependencies should be initialized.
While not causing any harm at the moment, suppose OZ someday decide to upgrade this contract and the sponsor uses the new version: it is possible that the contract will not work anymore.
Consider calling the init() here:

File: PromiseRouter.sol
23: contract PromiseRouter is Version, Router, ReentrancyGuardUpgradeable {
...
146:   function initialize(address _xAppConnectionManager) public initializer {
147:     __XAppConnectionClient_initialize(_xAppConnectionManager);
+ 147:     __ReentrancyGuard_init(); //@audit ReentrancyGuardUpgradeable
148:   }

This is already applied L72 in StableSwap.sol (but was forgotten in PromiseRouter.sol):

File: StableSwap.sol
61:   function initialize(
...
70:   ) public override initializer {
71:     __OwnerPausable_init();
72:     __ReentrancyGuard_init();

6. Deprecated safeApprove() function

Deprecated

Using this deprecated function can lead to unintended reverts and potentially the locking of funds. A deeper discussion on the deprecation of this function is in OZ issue #2219 (OpenZeppelin/openzeppelin-contracts#2219). The OpenZeppelin ERC20 safeApprove() function has been deprecated, as seen in the comments of the OpenZeppelin code.

As recommended by the OpenZeppelin comment, consider replacing safeApprove() with safeIncreaseAllowance() or safeDecreaseAllowance() instead:

core/connext/libraries/AssetLogic.sol:347:        SafeERC20.safeApprove(IERC20(_assetIn), address(pool), _amountIn);

7. Missing address(0) checks

Consider adding an address(0) check for immutable variables:

  • File: Executor.sol
29:  address private immutable connext;
...
47:   constructor(address _connext) {
+ 48:     require(_connext != address(0));
48:     connext = _connext;
49:   }

8. Misleading comment

Issue with comment

I suspect a copy-paste error here:

- core/connext/libraries/SwapUtils.sol:790:    require(dx <= maxDx, "Swap didn't result in min tokens");
+ core/connext/libraries/SwapUtils.sol:790:    require(dx <= maxDx, "Swap needs more than max tokens");

9. Add a timelock to critical functions

It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious owner making a sandwich attack on a user).

Consider adding a timelock to:

core/connext/facets/StableSwapFacet.sol:469:  function setSwapAdminFee(bytes32 canonicalId, uint256 newAdminFee) external onlyOwner {
core/connext/facets/StableSwapFacet.sol:478:  function setSwapFee(bytes32 canonicalId, uint256 newSwapFee) external onlyOwner {
core/connext/helpers/StableSwap.sol:448:  function setAdminFee(uint256 newAdminFee) external onlyOwner {
core/connext/helpers/StableSwap.sol:456:  function setSwapFee(uint256 newSwapFee) external onlyOwner {

10. abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256()

Similar issue in the past: here

Use abi.encode() instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456) => 0x123456 => abi.encodePacked(0x1,0x23456), but abi.encode(0x123,0x456) => 0x0...1230...456). If there is only one argument to abi.encodePacked() it can often be cast to bytes() or bytes32() instead.

core/connext/helpers/BridgeToken.sol:134:    bytes32 _digest = keccak256(abi.encodePacked(_EIP712_PREFIX_AND_VERSION, domainSeparator(), _hashStruct));
core/connext/libraries/ConnextMessage.sol:178:    return keccak256(abi.encodePacked(bytes(_name).length, _name, bytes(_symbol).length, _symbol, _decimals));

11. Upgradeable contract is missing a __gap[50] storage variable to allow for new storage variables in later versions

See this link for a description of this storage variable. While some contracts may not currently be sub-classed, adding the variable now protects against forgetting to add it in the future:

  • LPToken.sol
  • OwnerPausableUpgradeable.sol

Those contracts do apply the recommendation though:

  • ProposedOwnableUpgradeable.sol
  • BridgeToken.sol
  • ProposedOwnable.sol
  • Router.sol
  • XAppConnectionClient.sol

12. All initialize() functions are front-runnable in the solution

Consider adding some access control to them or deploying atomically or using constructor initializer:

core/connext/helpers/BridgeToken.sol:34:  function initialize() public override initializer {
core/connext/helpers/LPToken.sol:21:  function initialize(string memory name, string memory symbol) external initializer returns (bool) {
core/connext/helpers/StableSwap.sol:61:  function initialize(
core/connext/helpers/TokenRegistry.sol:73:  function initialize(address _tokenBeacon, address _xAppConnectionManager) public initializer {
core/connext/interfaces/IBridgeToken.sol:5:  function initialize() external;
core/connext/interfaces/IStableSwap.sol:99:  function initialize(
core/promise/PromiseRouter.sol:146:  function initialize(address _xAppConnectionManager) public initializer {
core/relayer-fee/RelayerFeeRouter.sol:80:  function initialize(address _xAppConnectionManager) public initializer {

13. Use the same revert string for consistency when testing the same condition

Issue with comment

Consider only using the shorter string:

core/connext/helpers/StableSwap.sol:155:    require(index < swapStorage.pooledTokens.length, "Out of range");
core/connext/helpers/StableSwap.sol:177:    require(index < swapStorage.pooledTokens.length, "Index out of range");

14. Use a constant instead of duplicating the same string

Issue with comment

core/connext/libraries/LibDiamond.sol:123:    require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)");
core/connext/libraries/LibDiamond.sol:141:    require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)");
core/connext/libraries/LibDiamond.sol:121:    require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut");
core/connext/libraries/LibDiamond.sol:139:    require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut");
core/connext/libraries/LibDiamond.sol:158:    require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut");
core/connext/libraries/SwapUtils.sol:493:    require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range");
core/connext/libraries/SwapUtils.sol:524:    require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range");
core/connext/libraries/SwapUtils.sol:662:    require(dy >= minDy, "Swap didn't result in min tokens");
core/connext/libraries/SwapUtils.sol:756:    require(dy >= minDy, "Swap didn't result in min tokens");
core/connext/libraries/SwapUtils.sol:703:    require(dx <= maxDx, "Swap needs more than max tokens");
core/connext/libraries/SwapUtils.sol:790:    require(dx <= maxDx, "Swap didn't result in min tokens"); //@audit this one is a copy-paste error
core/connext/libraries/SwapUtils.sol:697:    require(dy <= self.balances[tokenIndexTo], "Cannot get more than pool balance");
core/connext/libraries/SwapUtils.sol:784:    require(dy <= self.balances[tokenIndexTo], "Cannot get more than pool balance");
core/connext/libraries/SwapUtils.sol:717:      require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own");
core/connext/libraries/SwapUtils.sol:649:      require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own");
core/connext/libraries/SwapUtils.sol:750:    require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own");
core/connext/libraries/SwapUtils.sol:1071:    require(newAdminFee <= MAX_ADMIN_FEE, "Fee is too high");
core/connext/libraries/SwapUtils.sol:1084:    require(newSwapFee <= MAX_SWAP_FEE, "Fee is too high");

15. Use a 2-step ownership transfer pattern

Contracts inheriting from OpenZeppelin's libraries have the default transferOwnership() function (a one-step process). It's possible that the onlyOwner role mistakenly transfers ownership to a wrong address, resulting in a loss of the onlyOwner role.
Consider overriding the default transferOwnership() function to first nominate an address as the pendingOwner and implementing an acceptOwnership() function which is called by the pendingOwner to confirm the transfer.

core/connext/helpers/BridgeToken.sol:202:  function transferOwnership(address _newOwner) public override(IBridgeToken, OwnableUpgradeable) onlyOwner {
core/connext/helpers/BridgeToken.sol:203:    OwnableUpgradeable.transferOwnership(_newOwner);
core/connext/helpers/TokenRegistry.sol:302:    IBridgeToken(_token).transferOwnership(owner());
core/connext/interfaces/IBridgeToken.sol:29:  // inherited from ownable
core/connext/interfaces/IBridgeToken.sol:30:  function transferOwnership(address _newOwner) external;

16. A magic number should be documented and explained. Use a constant instead

Similar issue in the past: here

core/connext/facets/upgrade-initializers/DiamondInit.sol:70:      s.nonce = 0;
core/connext/facets/upgrade-initializers/DiamondInit.sol:76:      s.LIQUIDITY_FEE_NUMERATOR = 9995;
core/connext/facets/upgrade-initializers/DiamondInit.sol:77:      s.LIQUIDITY_FEE_DENOMINATOR = 10000;
core/connext/facets/upgrade-initializers/DiamondInit.sol:78:      s.maxRoutersPerTransfer = 5;
core/connext/helpers/TokenRegistry.sol:300:    IBridgeToken(_token).setDetails(_name, _symbol, 18);

Consider using constant variables as this would make the code more maintainable and readable while costing nothing gas-wise (constants are replaced by their value at compile-time).

17. Lack of event emission for operation changing the state

File: AssetFacet.sol
171:   function removeAssetId(bytes32 _canonicalId, address _adoptedAssetId) external onlyOwner {
...
179:     delete s.adoptedToLocalPools[_canonicalId]; // @audit-info INFO no event emitted for StableSwap removal

Non-Critical Issues

1. It's better to emit after all processing is done

  • contracts/contracts/core/connext/facets/AssetFacet.sol:
  152:     // Emit event
  153:     emit AssetAdded(_canonical.id, _canonical.domain, _adoptedAssetId, supported, msg.sender);
  154  
  155      // Add the swap pool
  156      _addStableSwapPool(_canonical, _stableSwapPool);
  157    }
  • contracts/contracts/core/connext/facets/RoutersFacet.sol:
  303:     // Emit event
  304:     emit RouterRemoved(router, msg.sender);
  305  
  306      // Remove router owner
  307      address _owner = s.routerPermissionInfo.routerOwners[router];
  308      if (_owner != address(0)) {
  309:       emit RouterOwnerAccepted(router, _owner, address(0));
  310        // delete routerOwners[router];
  311        s.routerPermissionInfo.routerOwners[router] = address(0);
  312      }
  316      if (_recipient != address(0)) {
  317:       emit RouterRecipientSet(router, _recipient, address(0));
  318        // delete routerRecipients[router];
  319        s.routerPermissionInfo.routerRecipients[router] = address(0);
  320      }
  335:     emit MaxRoutersPerTransferUpdated(_newMaxRouters, msg.sender);
  336  
  337      s.maxRoutersPerTransfer = _newMaxRouters;
  338    }
  • contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol:
  158    function setDirectPrice(address _token, uint256 _price) external onlyAdmin {
  159:     emit DirectPriceUpdated(_token, assetPrices[_token], _price);
  160      assetPrices[_token] = _price;
  161    }
  163    function setV1PriceOracle(address _v1PriceOracle) external onlyAdmin {
  164:     emit V1PriceOracleUpdated(v1PriceOracle, _v1PriceOracle);
  165      v1PriceOracle = _v1PriceOracle;
  166    }
  • contracts/contracts/core/connext/helpers/SponsorVault.sol:
  150:     emit RateUpdated(_originDomain, rates[_originDomain], _rate, msg.sender);
  151  
  152      rates[_originDomain] = _rate;
  153    }
  159    function setRelayerFeeCap(uint256 _relayerFeeCap) external onlyOwner {
  160:     emit RelayerFeeCapUpdated(relayerFeeCap, _relayerFeeCap, msg.sender);
  161      relayerFeeCap = _relayerFeeCap;
  162    }
  168    function setGasTokenOracle(address _gasTokenOracle) external onlyOwner {
  169:     emit GasTokenOracleUpdated(address(gasTokenOracle), _gasTokenOracle, msg.sender);
  170      gasTokenOracle = IGasTokenOracle(_gasTokenOracle);
  171    }
  181:     emit TokenExchangeUpdated(_token, address(tokenExchanges[_token]), _tokenExchange, msg.sender);
  182      tokenExchanges[_token] = ITokenExchange(_tokenExchange);
  183    }
  • contracts/contracts/core/connext/libraries/LibDiamond.sol:
  116:     emit DiamondCut(_diamondCut, _init, _calldata);
  117      initializeDiamondCut(_init, _calldata);
  118    }
  • contracts/contracts/core/promise/PromiseRouter.sol:
  256:     emit CallbackExecuted(transferId, msg.sender);
  257  
  258      // Should transfer the stored relayer fee to the msg.sender
  259      if (callbackFee > 0) {
  260        AddressUpgradeable.sendValue(payable(msg.sender), callbackFee);

2. The nonReentrant modifier should occur before all other modifiers

This is a best-practice to protect against re-entrancy in other modifiers

core/connext/facets/BridgeFacet.sol:279:  function xcall(XCallArgs calldata _args) external payable whenNotPaused nonReentrant returns (bytes32) {
core/connext/facets/BridgeFacet.sol:411:  function execute(ExecuteArgs calldata _args) external whenNotPaused nonReentrant returns (bytes32) {

3. Typos

  • funciton
core/connext/facets/upgrade-initializers/DiamondInit.sol:31:// of your diamond. Add parameters to the init funciton if you need to.
  • _potentialReplcia
core/connext/facets/BaseConnextFacet.sol:137:   * @notice Determine whether _potentialReplcia is an enrolled Replica from the xAppConnectionManager
  • bridgable
core/connext/facets/BridgeFacet.sol:265:   * assets will be swapped for their local nomad asset counterparts (i.e. bridgable tokens) via the configured AMM if
  • addressess
core/connext/facets/BridgeFacet.sol:774:      // Save the addressess of all routers providing liquidity for this transfer.
  • a sthe
core/connext/facets/BridgeFacet.sol:1059:    // Because we are using the `_amount` a sthe maximum amount in, the `amountIn` should always be
  • sournce
core/connext/facets/ProposedOwnableFacet.sol:143:    // Contract as sournce of truth
core/connext/facets/ProposedOwnableFacet.sol:163:    // Contract as sournce of truth
core/connext/facets/ProposedOwnableFacet.sol:176:    // Contract as sournce of truth
core/connext/helpers/ProposedOwnableUpgradeable.sol:170:    // Contract as sournce of truth
core/connext/helpers/ProposedOwnableUpgradeable.sol:198:    // Contract as sournce of truth
  • rlayer
core/connext/facets/RelayerFacet.sol:35:   * @notice Emitted when a rlayer is added or removed from whitelists
  • specicfied
core/connext/facets/RoutersFacet.sol:575:    // transfer to specicfied recipient IF recipient not set
  • callabale
core/connext/helpers/Executor.sol:17: * @notice This library contains an `execute` function that is callabale by
  • everytime
core/connext/helpers/LPToken.sol:41:   * minting and burning. This ensures that Swap.updateUserWithdrawFees are called everytime.
  • stabelswap
core/connext/libraries/AssetLogic.sol:30:   * @notice Check if the stabelswap pool exists or not
  • briding
core/connext/libraries/LibConnextStorage.sol:278:   * @notice Stores whether or not briding, AMMs, have been paused
  • identifer
core/connext/libraries/LibCrossDomainProperty.sol:35:  uint256 private constant PROPERTY_LEN = 25; // 1 byte identifer + 4 bytes domain + 20 bytes address
  • incomming
core/promise/PromiseRouter.sol:50:   * @dev While handling the message, it will parse transferId from incomming message and store the message in the mapping

4. Deprecated library used for Solidity >= 0.8 : SafeMath

core/connext/helpers/ConnextPriceOracle.sol:2:pragma solidity 0.8.14;
core/connext/helpers/ConnextPriceOracle.sol:4:import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";
core/connext/helpers/ConnextPriceOracle.sol:45:  using SafeMath for uint256;

core/connext/helpers/OZERC20.sol:2:pragma solidity 0.8.14;
core/connext/helpers/OZERC20.sol:10:import "@openzeppelin/contracts/utils/math/SafeMath.sol";
core/connext/helpers/OZERC20.sol:37:  using SafeMath for uint256;

core/connext/libraries/AmplificationUtils.sol:2:pragma solidity 0.8.14;
core/connext/libraries/AmplificationUtils.sol:5:import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";
core/connext/libraries/AmplificationUtils.sol:15:  using SafeMath for uint256;

core/connext/libraries/SwapUtils.sol:2:pragma solidity 0.8.14;
core/connext/libraries/SwapUtils.sol:4:import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol";
core/connext/libraries/SwapUtils.sol:20:  using SafeMath for uint256;

5. Open TODOS

Consider resolving the TODOs before deploying.

core/connext/facets/AssetFacet.sol:66:  function canonicalToAdopted(bytes32 _canonicalId) public view returns (address) {
core/connext/facets/AssetFacet.sol:67:    return s.canonicalToAdopted[_canonicalId];
core/connext/facets/AssetFacet.sol:150:    s.canonicalToAdopted[_canonical.id] = supported;
core/connext/facets/AssetFacet.sol:185:    delete s.canonicalToAdopted[_canonicalId];
core/connext/facets/BridgeFacet.sol:492:      // TODO: do we want to store a mapping of custodied token balances here?
core/connext/facets/BridgeFacet.sol:579:      // TODO: do we need to keep this
core/connext/facets/BridgeFacet.sol:1027:    // TODO: Should we call approve(0) and approve(totalRepayAmount) instead? or with a try catch to not affect gas on all cases?
core/connext/helpers/Executor.sol:7:// TODO: see note in below file re: npm
core/connext/interfaces/IConnextHandler.sol:22:  function canonicalToAdopted(bytes32 _canonicalId) external view returns (address);
core/connext/libraries/AssetLogic.sol:204:    address adopted = s.canonicalToAdopted[id];
core/connext/libraries/AssetLogic.sol:244:    address adopted = s.canonicalToAdopted[id];
core/connext/libraries/AssetLogic.sol:376:    address adopted = s.canonicalToAdopted[id];
core/connext/libraries/LibConnextStorage.sol:178:  mapping(bytes32 => address) canonicalToAdopted;
core/connext/libraries/LibConnextStorage.sol:303:  // BridgeFacet (cont.) TODO: can we move this

6. Adding a return statement when the function defines a named return variable, is redundant

While not consuming more gas with the Optimizer enabled: using both named returns and a return statement isn't necessary. Removing one of those can improve code clarity.

Affected code:

  • contracts/contracts/core/connext/facets/StableSwapFacet.sol:
  212:   ) external view returns (uint256 availableTokenAmount) {
  213:     return s.swapStorages[canonicalId].calculateWithdrawOneToken(tokenAmount, tokenIndex);
  • contracts/contracts/core/connext/helpers/StableSwap.sol:
  295:     returns (uint256 availableTokenAmount)
  297:     return swapStorage.calculateWithdrawOneToken(tokenAmount, tokenIndex);

7. The pragmas used are not the same everywhere

Consider using only 1 version. Here's an example of the different pragmas:

core/connext/facets/upgrade-initializers/DiamondInit.sol:2:pragma solidity ^0.8.0;
core/connext/facets/AssetFacet.sol:2:pragma solidity 0.8.14;
core/connext/interfaces/IAavePool.sol:2:pragma solidity ^0.8.11;
core/shared/Router.sol:2:pragma solidity >=0.6.11;

8. Non-library/interface files should use fixed compiler versions, not floating ones

core/connext/facets/upgrade-initializers/DiamondInit.sol:2:pragma solidity ^0.8.0;
core/shared/Router.sol:2:pragma solidity >=0.6.11;
core/shared/XAppConnectionClient.sol:2:pragma solidity >=0.6.11;

9. Missing NatSpec

File: AssetFacet.sol
121:   /**
122:    * @notice Used to add supported assets. This is an admin only function
123:    * @dev When whitelisting the canonical asset, all representational assets would be
124:    * whitelisted as well. In the event you have a different adopted asset (i.e. PoS USDC
125:    * on polygon), you should *not* whitelist the adopted asset. The stable swap pool
126:    * address used should allow you to swap between the local <> adopted asset
127:    * @param _canonical - The canonical asset to add by id and domain. All representations
128:    * will be whitelisted as well
129:    * @param _adoptedAssetId - The used asset id for this domain (i.e. PoS USDC for
130:    * polygon)
131:    */
132:   function setupAsset(
133:     ConnextMessage.TokenId calldata _canonical,
134:     address _adoptedAssetId,
135:     address _stableSwapPool // @audit-info [INFO] NatSpec missing for _stableSwapPool
136:   ) external onlyOwner {
@code423n4 code423n4 added bug Something isn't working QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax labels Jun 19, 2022
code423n4 added a commit that referenced this issue Jun 19, 2022
@jakekidd
Copy link
Collaborator

jakekidd commented Jul 2, 2022

great linking, great format!

@jakekidd
Copy link
Collaborator

jakekidd commented Jul 2, 2022

1 is invalid

all of these seem non-critical except for 15, which is acknowledged and 3/12, which are valid

@0xleastwood
Copy link
Collaborator

I would tend to agree that all of these are valid except for:

  1. Check that _sponsorVault is a contract

Even if we assert that this address is a contract, the issue whereby the owner role can force _handleExecuteTransaction to revert still stands. They can implement the reimburseLiquidityFees or reimburseRelayerFees functions such that they revert upon being called. Best solution to this would be to utilise some try/catch statement on these calls instead of asserting that this address is a contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax
Projects
None yet
Development

No branches or pull requests

3 participants