Platform: Code4rena
Start Date: 08/06/2022
Pot Size: $115,000 USDC
Total HM: 26
Participants: 72
Period: 11 days
Judge: leastwood
Total Solo HM: 14
Id: 132
League: ETH
Rank: 8/72
Findings: 4
Award: $2,909.39
🌟 Selected for report: 1
🚀 Solo Findings: 0
2338.3143 USDC - $2,338.31
The caller of repayAavePortal()
can trigger an underflow to arbitrarily increase the caller's balance through an underflow.
// Relevant code sections: // PortalFacet.sol function repayAavePortal( address _local, uint256 _backingAmount, uint256 _feeAmount, uint256 _maxIn, bytes32 _transferId ) external { uint256 totalAmount = _backingAmount + _feeAmount; // in adopted uint256 routerBalance = s.routerBalances[msg.sender][_local]; // in local // Sanity check: has that much to spend if (routerBalance < _maxIn) revert PortalFacet__repayAavePortal_insufficientFunds(); // Need to swap into adopted asset or asset that was backing the loan // The router will always be holding collateral in the local asset while the loaned asset // is the adopted asset // Swap for exact `totalRepayAmount` of adopted asset to repay aave (bool success, uint256 amountIn, address adopted) = AssetLogic.swapFromLocalAssetIfNeededForExactOut( _local, totalAmount, _maxIn ); if (!success) revert PortalFacet__repayAavePortal_swapFailed(); // decrement router balances unchecked { s.routerBalances[msg.sender][_local] -= amountIn; } // back loan _backLoan(_local, _backingAmount, _feeAmount, _transferId); } // AssetLogic.sol function swapFromLocalAssetIfNeededForExactOut( address _asset, uint256 _amount, uint256 _maxIn ) internal returns ( bool, uint256, address ) { AppStorage storage s = LibConnextStorage.connextStorage(); // Get the token id (, bytes32 id) = s.tokenRegistry.getTokenId(_asset); // If the adopted asset is the local asset, no need to swap address adopted = s.canonicalToAdopted[id]; if (adopted == _asset) { return (true, _amount, _asset); } return _swapAssetOut(id, _asset, adopted, _amount, _maxIn); }
First, call repayAavePortal()
where _backingAmount + _feeAmount > s.routerBalances[msg.sender][_local] && _maxIn > s.routerBalances[msg.sender][_local]
. That will trigger the call to the AssetLogic contract:
(bool success, uint256 amountIn, address adopted) = AssetLogic.swapFromLocalAssetIfNeededForExactOut( _local, totalAmount, _maxIn );
By setting _local
to the same value as the adopted asset, you trigger the following edge case:
address adopted = s.canonicalToAdopted[id]; if (adopted == _asset) { return (true, _amount, _asset); }
So the amountIn
value returned by swapFromLocalAssetIfNeededForExactOut()
is the totalAmount
value that was passed to it. And totalAmount == _backingAmount + _feeAmount
.
Meaning the amountIn
value is user-specified for this edge case. Finally, we reach the following line:
unchecked { s.routerBalances[msg.sender][_local] -= amountIn; }
amountIn
(user-specified) is subtracted from the routerBalances
in an unchecked
block. Thus, the attacker is able to trigger an underflow and increase their balance arbitrarily high. The repayAavePortal()
function only verifies that routerBalance < _maxIn
.
Here's a test as PoC:
// PortalFacet.t.sol function test_PortalFacet_underflow() public { s.routerPermissionInfo.approvedForPortalRouters[router] = true; uint backing = 2 ether; uint fee = 10000; uint init = 1 ether; s.routerBalances[router][_local] = init; s.portalDebt[_id] = backing; s.portalFeeDebt[_id] = fee; vm.mockCall(s.aavePool, abi.encodeWithSelector(IAavePool.backUnbacked.selector), abi.encode(true)); vm.prank(router); this.repayAavePortal(_local, backing, fee, init - 0.5 ether, _id); // balance > init => underflow require(s.routerBalances[router][_local] > init); }
none
After the call to swapFromLocalAssetIfNeededForExactOut()
you should add the following check:
if (_local == adopted) { require(routerBalance >= amountIn); }
#0 - LayneHaber
2022-06-27T15:31:01Z
#1 - 0xleastwood
2022-08-02T05:03:42Z
This is entirely valid and a really severe issue. If the local asset is the adopted asset, AssetLogic.swapFromLocalAssetIfNeededForExactOut()
will return amountIn == totalAmount
. So in order to overflow routerBalances
, the router just needs to provide _backingAmount + _feeAmount
inputs that sum to exceed the router's current balance.
🌟 Selected for report: xiaoming90
As you described in your own comment, certain ERC20 tokens, e.g. USDT, don't allow you to set a new allowance if the existing one is not 0. If, as you say, backUnbacked()
doesn't pull all the funds, you will run into a scenario where further calls to _reconcileProcessPortal()
will fail.
none
Set the allowance to 0 first before approving the new value.
#0 - jakekidd
2022-06-26T19:09:36Z
#1 - 0xleastwood
2022-08-13T22:29:55Z
I think this deserves to be a duplicate of #154 because it clearly outlines how _reconcileProcessPortal
will be broken as a result.
🌟 Selected for report: BowTiedWardens
Also found by: 0x1f8b, 0x29A, 0x52, 0xNazgul, 0xNineDec, 0xf15ers, 0xkatana, 0xmint, Chom, ElKu, Funen, IllIllI, JMukesh, Jujic, Kaiziron, Lambda, MiloTruck, Ruhum, SmartSek, SooYa, TerrierLover, TomJ, WatchPug, Waze, _Adam, asutorufos, auditor0517, bardamu, c3phas, catchup, cccz, ch13fd357r0y3r, cloudjunky, cmichel, cryptphi, csanuragjain, defsec, fatherOfBlocks, hansfriese, hyh, jayjonah8, joestakey, k, kenta, obtarian, oyc_109, robee, sach1r0, shenwilly, simon135, slywaters, sorrynotsorry, tintin, unforgiven, xiaoming90, zzzitron
145.6501 USDC - $145.65
PromiseRouter.initialize()
doesn't initialize the ReentrancyUpgradable dependencyIn your initialize()
function you should also call the initialize()
functions of all the other contracts you inherit. In this case, the ReentrancyGuardUpgradable one is missing.
Relevant code:
You already do that in a couple of spots. But, some are missing. For example, the ConnextPriceOracle contract doesn't use it to change the admin address
Relevant code:
Throughout the codebase there are multiple open TODOs.
.//contracts_forge/core/connext/facets/RoutersFacet.t.sol:53: // TODO: test modifiers? onlyRouterOwner, onlyProposedRouterOwner, etc. .//contracts_forge/core/connext/facets/BridgeFacet.t.sol:468: // TODO: For some reason, balance isn't changing. Perhaps the vm.mockCall prevents this? .//contracts_forge/core/connext/facets/BridgeFacet.t.sol:1310: // TODO: fails if destination domain does not have an xapp router registered .//contracts_forge/core/connext/facets/AssetFacet.t.sol:147: // TODO: test_adminFunctions__onlyOwner ?? .//contracts_forge/core/connext/facets/RelayerFacet.t.sol:44: // TODO: onlyRelayerFeeRouter .//contracts_forge/Connext.t.sol:352: // TODO Correctly calculate the message .//contracts/core/connext/facets/BridgeFacet.sol:496: // TODO: do we want to store a mapping of custodied token balances here? .//contracts/core/connext/facets/BridgeFacet.sol:583: // TODO: do we need to keep this .//contracts/core/connext/facets/BridgeFacet.sol:1031: // TODO: Should we call approve(0) and approve(totalRepayAmount) instead? or with a try catch to not affect gas on all cases? .//contracts/core/connext/libraries/LibConnextStorage.sol:304: // BridgeFacet (cont.) TODO: can we move this .//contracts/core/connext/helpers/Executor.sol:7:// TODO: see note in below file re: npm .//contracts/nomad-core/contracts/upgrade/UpgradeBeaconController.sol:17:// TODO: upgrade to `ProposedOwnable` .//contracts/nomad-core/libs/ExcessivelySafeCall.sol:7:// TODO: Update to npm version when these changes are reflected
#0 - jakekidd
2022-07-01T22:42:05Z
01: invalid - this is handled in BaseConnextFacet.init method
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, 0xmint, BowTiedWardens, ElKu, Fitraldys, Funen, Kaiziron, Lambda, Metatron, MiloTruck, Randyyy, Ruhum, SmartSek, TomJ, Tomio, UnusualTurtle, Waze, _Adam, apostle0x01, asutorufos, c3phas, catchup, csanuragjain, defsec, fatherOfBlocks, hansfriese, hyh, ignacio, joestakey, k, kaden, nahnah, oyc_109, rfa, robee, sach1r0, simon135, slywaters
84.4973 USDC - $84.50
Packing structs will reduce gas consumption, see https://dev.to/javier123454321/solidity-gas-optimizations-pt-3-packing-structs-23f4
Relevant code: