Platform: Code4rena
Start Date: 30/05/2023
Pot Size: $300,500 USDC
Total HM: 79
Participants: 101
Period: about 1 month
Judge: Trust
Total Solo HM: 36
Id: 242
League: ETH
Rank: 14/101
Findings: 6
Award: $4,852.79
🌟 Selected for report: 3
🚀 Solo Findings: 2
576.0794 USDC - $576.08
Increasing weights adds more weight than expected, which breaks rebalancing functionality down the line.
A pool weight can be increased with setWeight
of UlyssesPool. Once the weight is increased, bandwidth is redistributed to the target poolId
first. Afterward, any leftOverBandwidth
should be distributed to the other pools, pro rata. However, this is not done correctly.
According to the code, the bandwidthStateList
is looped over and the leftOverBandwidth
is distributed proportional to their respective weights, which works as intended. Once it reaches the last pool on the list, the entire leftOverBandwidth
is added to it. This leads to extra bandwidth to the last pool.
Here is the gist that tries to distribute the leftOverBandwidth
.
function setWeight(uint256 poolId, uint8 weight) external nonReentrant onlyOwner { ... for (uint256 i = 1; i < bandwidthStateList.length;) { if (i != poolIndex) { if (i == bandwidthStateList.length - 1) { // @audit Last pool always receives entire leftOverBandwidth bandwidthStateList[i].bandwidth += leftOverBandwidth.toUint248(); } else if (leftOverBandwidth > 0) { bandwidthStateList[i].bandwidth += leftOverBandwidth.mulDiv(bandwidthStateList[i].weight, weightsWithoutPool).toUint248(); } } unchecked { ++i; } } }
Consider subtracting from leftOverBandwidth
after each loop iteration. Once the loop gets to the last item, it should be the correct amount.
Math
#0 - c4-judge
2023-07-07T10:35:03Z
trust1995 marked the issue as satisfactory
#1 - c4-judge
2023-07-07T10:35:07Z
trust1995 marked the issue as primary issue
#2 - trust1995
2023-07-07T10:35:26Z
I think it is intentional but would like sponsor to double-confirm.
#3 - c4-sponsor
2023-07-11T19:43:06Z
0xLightt marked the issue as sponsor confirmed
#4 - c4-judge
2023-07-25T13:52:26Z
trust1995 marked the issue as selected for report
#5 - c4-judge
2023-07-26T14:02:22Z
trust1995 marked the issue as not selected for report
#6 - c4-judge
2023-07-26T14:02:37Z
trust1995 marked the issue as duplicate of #766
#7 - c4-judge
2023-07-27T11:04:14Z
trust1995 changed the severity to 3 (High Risk)
576.0794 USDC - $576.08
Decreasing weights causes an underflow error, which breaks functionality.
A pool weight can be decreased with setWeight
of UlyssesPool. Once the weight is decreased for a target poolId
, the other pool bandwidths are increased, and then leftOverBandwidth
is calculated.
if (oldTotalWeights > newTotalWeights) { for (uint256 i = 1; i < bandwidthStateList.length;) { if (i != poolIndex) { uint256 oldBandwidth = bandwidthStateList[i].bandwidth; if (oldBandwidth > 0) { // @audit This will increase bandwidth to an amount higher than oldBandwidth bandwidthStateList[i].bandwidth = oldBandwidth.mulDivUp(oldTotalWeights, newTotalWeights).toUint248(); // @audit if oldbandwidth > the new bandwidthStateList[i].bandwidth, this will cause an underflow leftOverBandwidth += oldBandwidth - bandwidthStateList[i].bandwidth; } } unchecked { ++i; } } poolState.bandwidth += leftOverBandwidth.toUint248(); // @audit-info increasing weight (newTotalWeights > oldTotalWeights) }
However, this will not work correctly as calculating leftOverBandwidth
will cause an underflow. Since oldTotalWeights
is greater than newTotalWeights
, the new bandwidthStateList[i].bandwidth
will always be greater than oldBandwidth
.
This is exemplified below:
Given:
oldTotalWeights
: 20newTotalWeights
: 10oldBandwidth
: 100The calculation for bandwidthStateList[i].bandwidth
will be 100 x 20 / 10 = 200
. Afterwards, leftOverBandwidth
will be calculated as 100 - 200 = -100
. An underflow will occur.
Manual
Consider changing this line to:
leftOverBandwidth += bandwidthStateList[i].bandwidth - oldBandwidth;
Under/Overflow
#0 - c4-judge
2023-07-07T10:50:05Z
trust1995 marked the issue as satisfactory
#1 - c4-judge
2023-07-07T10:50:10Z
trust1995 marked the issue as primary issue
#2 - c4-sponsor
2023-07-11T19:49:37Z
0xLightt marked the issue as sponsor confirmed
#3 - 0xLightt
2023-07-11T20:02:32Z
Hey, this is true, but I believe the real issue is in the if (oldTotalWeights > newTotalWeights)
here.
It should be the other way around if (oldTotalWeights < newTotalWeights)
:
This would lead to the same issue described in this issue here, but changing the decreased bandwidth calculation from poolState.bandwidth = oldBandwidth.mulDivUp(oldTotalWeights, newTotalWeights).toUint248();
to poolState.bandwidth = oldBandwidth.mulDivUp(newTotalWeights, oldTotalWeights).toUint248();
would be the ideal to address this so we are removing from the bandwidth.
#4 - c4-judge
2023-07-25T13:51:24Z
trust1995 marked the issue as selected for report
#5 - c4-judge
2023-07-26T14:02:20Z
trust1995 marked the issue as not selected for report
#6 - c4-judge
2023-07-26T14:02:34Z
trust1995 marked the issue as duplicate of #766
#7 - c4-judge
2023-07-27T11:04:14Z
trust1995 changed the severity to 3 (High Risk)
576.0794 USDC - $576.08
Target pool bandwidth calculation is incorrect, which leads to incorrect pool balancing.
In setWeights
of UlyssesPool.sol, the leftOverBandwidth
gets added to the target poolState.bandwidth
. As discussed with the Sponsor, this is incorrect because when weight is decreased, the bandwidth should decrease to distribute to the other pools.
This PoC uses a new test file: UlyssesFactoryTest.t.sol
. Run the test by calling forge test --match-test testReduceWeightIncreasesBandwidth
import {Test, StdUtils} from "forge-std/Test.sol"; import {UlyssesFactory} from "@ulysses-amm/factories/UlyssesFactory.sol"; import {UlyssesPool} from "@ulysses-amm/UlyssesPool.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; contract InvariantUlyssesFactory is Test { struct BandwidthState { uint248 bandwidth; uint8 weight; UlyssesPool destination; } UlyssesFactory ulyssesFactory; ERC20 token1; ERC20 token2; address alice = address(1); function setUp() public { ulyssesFactory = new UlyssesFactory(address(this)); token1 = new MockERC20("Token1", "TK1", 18); token2 = new MockERC20("Token2", "TK2", 18); deal(address(token1), alice, 1 ether); } function testReduceWeightIncreasesBandwidth() public { ERC20[] memory assets = new ERC20[](4); assets[0] = token1; assets[1] = token1; assets[2] = token1; assets[3] = token1; uint8[][] memory weights = new uint8[][](4); // outer // Initialize the inner arrays weights[0] = new uint8[](4); weights[1] = new uint8[](4); weights[2] = new uint8[](4); weights[3] = new uint8[](4); // Pool1 weights weights[0][0] = 5; weights[0][1] = 10; weights[0][2] = 10; weights[0][3] = 10; // Pool2 weights weights[1][0] = 5; weights[1][1] = 10; weights[1][2] = 10; weights[1][3] = 10; // Pool3 weights weights[2][0] = 5; weights[2][1] = 10; weights[2][2] = 10; weights[2][3] = 10; // Pool4 weights weights[3][0] = 5; weights[3][1] = 10; weights[3][2] = 10; weights[3][3] = 10; uint256[] memory poolIds = ulyssesFactory.createPools(assets, weights, address(this)); UlyssesPool pool1 = ulyssesFactory.pools(poolIds[0]); // id = 2 vm.startPrank(alice); token1.approve(address(pool1), 0.1 ether); pool1.deposit(0.1 ether, alice); vm.stopPrank(); uint256 pool1BandwidthBefore = pool1.getBandwidth(3); // Set weight to lower pool1.setWeight(3, 5); uint256 pool1BandwidthAfter = pool1.getBandwidth(3); assertEq(pool1BandwidthAfter > pool1BandwidthBefore, true); } }
Manual
Consider changing this line to:
poolState.bandwidth -= leftOverBandwidth.toUint248();
Math
#0 - c4-judge
2023-07-07T10:56:23Z
trust1995 marked the issue as satisfactory
#1 - c4-judge
2023-07-07T10:56:27Z
trust1995 marked the issue as primary issue
#2 - c4-judge
2023-07-11T17:17:27Z
trust1995 marked the issue as duplicate of #772
#3 - c4-judge
2023-07-11T17:17:42Z
trust1995 changed the severity to 3 (High Risk)
#4 - c4-judge
2023-07-26T14:02:20Z
trust1995 marked the issue as duplicate of #766
🌟 Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
95.3782 USDC - $95.38
depositToPort()
mints incorrect hToken amounts because token mints are inflated/deflated depending on the underlying decimal.
In depositToPort()
of ArbitrumBranchPort.sol, the BridgeAgent transfers an underlying _deposit
amount from the depositor
, and subsequently mints the same _deposit
amount. This is fine when the underlying decimal is 18
, but is incorrect with decimals not equal to 18
. The amount that is transferred and minted should not use the same _decimal
variable. Instead, the mint amount should be a denormalized amount i.e., converted to 1e18 equivalent amount.
For example, given an underlying token with a decimal of 6, and the _deposit
amount of 1e6
. Currently, the amount transferred will be 1e6
, which is correct. But, the amount minted will also be 1e6
. This means that tokens with higher decimals will mint more hTokens. As discussed with the Sponsor, the amount should be 1e18
in this case.
Manual review
Consider denormalization of the _deposit
amount to 1e18 for the mintToLocalBranch()
amount.
function depositToPort(address _depositor, address _recipient, address _underlyingAddress, uint256 _deposit) external requiresBridgeAgent { address globalToken = IRootPort(rootPortAddress).getLocalTokenFromUnder(_underlyingAddress, localChainId); if (globalToken == address(0)) revert UnknownUnderlyingToken(); _underlyingAddress.safeTransferFrom(_depositor, address(this), _deposit); IRootPort(rootPortAddress).mintToLocalBranch(_recipient, globalToken, _denormalizeDecimals(_deposit)); }
Math
#0 - c4-judge
2023-07-09T15:24:44Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-09T15:24:49Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T11:29:35Z
trust1995 marked the issue as partial-50
#3 - c4-judge
2023-07-25T11:29:43Z
trust1995 marked the issue as full credit
#4 - c4-judge
2023-07-25T14:00:09Z
trust1995 marked the issue as satisfactory
🌟 Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
95.3782 USDC - $95.38
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L1340-L1342 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L687 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L252 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L284 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L341-L343
Functions that rely on _normalizeDecimals()
does not work as intended. It does not handle decimals other than 18
correctly. This results in incorrect accounting down the line.
Throughout BranchBridgeAgent.sol, _normalizeDecimals()
is used to convert an underlying deposit amount to 18 decimal places. After discussing with the Sponsor, _normalizeDecimals()
is intended to convert an amount to 1e18
. However, it does not work correctly for decimals other than 18
.
Take for example, when a user calls BaseBranchRouter.callOutAndBridge()
with dParams.deposits
of 1e6
(token decimal 6
), the subsequent call of BranchBridgeAgent._callOutAndBridge()
will 1) deposit 1e6
tokens on behalf of the user, and 2) pack the value of _normalizeDecimals(1e6, 6)
. In this case, it will be 1e6 * (10 ** 6) / 1 ether
, which results in 0
.
When the data reaches the Root chain, no RootTokens will be minted despite the user depositing 1e6
underlying tokens.
A similar, but opposite, scenario can happen with token decimals larger than 18.
Manual
Consider fixing _normalizeDecimals()
to handle decimals other than 18.
Math
#0 - c4-judge
2023-07-09T15:25:09Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-09T15:25:14Z
trust1995 marked the issue as satisfactory
🌟 Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
95.3782 USDC - $95.38
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/RootPort.sol#L282 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchPort.sol#L248 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchPort.sol#L275
Root hTokens will be minted based on the decimals of the underlying deposit, which causes incorrect accounting.
In bridgeToRoot()
of RootPort.sol, the root hToken is transferred to the _recipient
. The amount that is transferred is the difference between _amount
and _deposit
. As discussed with the Sponsor, _amount
is an amount of hTokens, which is decimal 18
. The _deposit
is the deposit amount of underlying, which can have varying decimals. This results in incorrect accounting, because _deposit
amounts using decimals lower than 18
will result in more tokens transferred. Tokens with decimals greater than 18
, can result in an underflow because _deposit
is greater than _amount
.
There are other places where amount - deposit
is used, but does not take into account the deposit
decimals, such as:
Manual
Consider converting _deposit
to 18
decimals to match the hToken decimals.
Decimal
#0 - c4-judge
2023-07-10T10:05:38Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-10T10:05:43Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T11:29:21Z
trust1995 marked the issue as partial-50
#3 - c4-judge
2023-07-28T11:09:22Z
trust1995 marked the issue as full credit
#4 - c4-judge
2023-07-31T14:27:48Z
trust1995 marked the issue as satisfactory
🌟 Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
95.3782 USDC - $95.38
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L948-L952 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L984 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchPort.sol#L212
Underlying token amounts are incorrect when withdrawing deposits.
In _redeemDeposit()
of BranchBridgeAgent.sol, a user can withdraw their failed deposit. First, the deposits
amounts are fetched by the _depositNonce
. Next, clearToken()
is called to mint and withdraw the local port and underlying tokens, respectively. When withdraw()
is called, the _deposit
amount is first converted using _denormalizeDecimals()
. However, the result of the conversion is incorrect when token decimals are not 18
.
Here are some examples when _denormalizeDecimals()
is called:
_deposit
of 1e6
, decimal 6: 1e6 * 1 ether / (10 ** 6)
= 1e18
_deposit
of 1e20
, decimal 20: 1e20 * 1 ether / (10 ** 20)
= 1e18
In both cases, the result is 1e18
which is incorrect because the underlying amount should be in their respective decimals. In both cases, using _deposit
amount would be correct.
Manual
Consider using the stored _deposit
amount to transfer the underlying amount.
Decimal
#0 - c4-judge
2023-07-10T10:08:44Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-10T10:08:48Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T11:29:13Z
trust1995 marked the issue as partial-50
#3 - c4-judge
2023-07-28T11:09:31Z
trust1995 marked the issue as full credit
#4 - c4-judge
2023-07-31T14:28:08Z
trust1995 marked the issue as satisfactory
🌟 Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
95.3782 USDC - $95.38
Deposit amount of tokens of varying decimals are incorrectly converted, leading to incorrect accounting.
In bridgeOut()
of BranchPort.sol, the underlying token is transferred from the _depositor
to the BranchPort. The _deposit
amount is first converted using _denormalizeDecimals()
. However, the result of the conversion is incorrect when token decimals are not 18
.
Here are some examples when _denormalizeDecimals()
is called::
_deposit
of 1e6
, decimal 6: 1e6 * 1 ether / (10 ** 6)
= 1e18
_deposit
of 1e20
, decimal 20: 1e20 * 1 ether / (10 ** 20)
= 1e18
In both cases, the result is 1e18
which is incorrect because the underlying amount should be in their respective decimals. In both cases, using _deposit
amount would be correct.
Manual
Consider directly using _deposit
since it is already in the correct decimal places.
Decimal
#0 - c4-judge
2023-07-10T10:09:13Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-10T10:09:17Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T11:29:04Z
trust1995 marked the issue as partial-50
#3 - c4-judge
2023-07-28T11:09:45Z
trust1995 marked the issue as full credit
#4 - c4-judge
2023-07-31T14:28:23Z
trust1995 marked the issue as satisfactory
🌟 Selected for report: ltyu
Also found by: BPZ, Koolex, RED-LOTUS-REACH, xuwinnie, yellowBirdy
561.6774 USDC - $561.68
https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/BranchBridgeAgent.sol#L1006-L1011 https://github.com/code-423n4/2023-05-maia/blob/54a45beb1428d85999da3f721f923cbf36ee3d35/src/ulysses-omnichain/lib/AnycallFlags.sol#L11
Cross-chain calls will fail since source-fee is not supplied to Anycall
In _performCall()
of BranchBridgeAgent.sol, a cross-chain called is made using anyCall()
with the _flag
of 4. According to the Anycall V7 documentation and code, when using gas _flag
of 4, the gas fee must be paid on the source chain. This means anyCall()
must be called and sent gas.
However, this is not the case, and the result is _performCall
will always revert. This will impact many functions that rely on this function such as callOut()
, callOutSigned()
, retryDeposit()
, and etc.
Manual
After discussing with the Sponsor, it is expected that the fee be paid on the destination chain, specifically the rootBridgeAgent
. Consider refactoring the code to change the _flag
to use pay on destination.
Alternatively, if pay on source is the intention, consider refactoring the code to include fees, starting with _performCall
. Additional refactoring will be required.
function _performCall(bytes memory _calldata, uint256 _fee) internal virtual { //Sends message to AnycallProxy IAnycallProxy(localAnyCallAddress).anyCall{value: _fee}( rootBridgeAgentAddress, _calldata, rootChainId, AnycallFlags.FLAG_ALLOW_FALLBACK, "" ); }
Library
#0 - c4-judge
2023-07-09T11:58:07Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-09T12:01:43Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-11T19:24:52Z
0xLightt marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-25T13:48:57Z
trust1995 marked the issue as selected for report
#4 - c4-sponsor
2023-07-27T19:11:42Z
0xBugsy marked the issue as sponsor acknowledged
#5 - c4-sponsor
2023-07-28T13:24:51Z
0xBugsy marked the issue as sponsor confirmed
#6 - 0xBugsy
2023-07-28T13:24:54Z
We recognize the audit's findings on Anycall. These will not be rectified due to the upcoming migration of this section to LayerZero.
🌟 Selected for report: ltyu
1712.1701 USDC - $1,712.17
Certain functions require native tokens to be sent. These functions will revert.
According to the Sponsor, VirtualAccounts can "call any of the dApps present in the Root Chain (Arbitrum) e.g. Maia, Hermes, Ulysses AMM,Uniswap." However, this is not the case as call()
is not payable
and thus cannot send native tokens to other contracts. This is problematic because certain functions require native token transfers and will fail.
Manual
Consider creating a single call()
function that has a payable
modifier and {value: msg.value}
. Be aware that since calls[i].target.call()
is in a loop, it is not advisable to add payable to the existing call()
. This is because msg.value
may be used multiple times, and is unsafe.
Payable
#0 - c4-judge
2023-07-10T11:14:49Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-10T11:14:54Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T13:54:48Z
0xBugsy marked the issue as sponsor acknowledged
#3 - c4-sponsor
2023-07-12T13:54:53Z
0xBugsy marked the issue as disagree with severity
#4 - trust1995
2023-07-25T08:31:55Z
Breaking of interoperability with dApps on the hosting chain, contrary to docs, justifies Med severity in my opinion.
#5 - c4-judge
2023-07-25T13:36:22Z
trust1995 marked the issue as selected for report
#6 - c4-sponsor
2023-07-28T16:09:50Z
0xBugsy marked the issue as sponsor confirmed
#7 - 0xLightt
2023-09-07T10:44:18Z
🌟 Selected for report: ltyu
1712.1701 USDC - $1,712.17
In createPools
of UlyssesFactory.sol, the return parameter poolIds
is used to store new pool ids after creation. However, it has not been initialized. This causes an index out of bounds error when createPools
is called.
Any test that calls ulyssesFactory.createPools(...);
will cause index out of bounds
Manual review
Consider adding this line:
poolIds = new uint256[](length);
Invalid Validation
#0 - c4-judge
2023-07-07T10:46:30Z
trust1995 marked the issue as satisfactory
#1 - c4-judge
2023-07-07T10:46:35Z
trust1995 marked the issue as primary issue
#2 - c4-sponsor
2023-07-11T19:44:14Z
0xLightt marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-25T13:51:30Z
trust1995 marked the issue as selected for report
#4 - 0xLightt
2023-07-28T13:04:50Z
We recognize the audit's findings on Ulysses AMM. These will not be rectified due to the upcoming migration of this section to Balancer Stable Pools.
195.3093 USDC - $195.31
As there was only so much time for such a large codebase, this audit was partially completed and the analysis is provided in the context of the completed modules. The modules that were completed are: Ulysses (AMM and Omnichain), and Hermes (with its dependencies).
Overall, the codebase is clean, with a lot of comments, and supporting documentation. The use of natspec when needed was not distracting, but was helpful where a little extra color was needed, such as when assembly was used.
Whether the devs are aware or not, I would like to gently remind that the use of SafeTransferLib may only give the disguise of being safe. The Library is intended to make token transfers safer by handling missing return values. However, as noted in the natspec, the Library does not check if the token has code. Keep this in mind in the future if return values are relied on.
Documentation I think the documentation could be more technical. The documentation has a lot of high level concepts that were hard to connect to the code without clarification.
For example, in the first paragraph of "Virtual Liquidity"
Virtual Liquidity refers to the mirroring of assets locked in different chains within a single liquidity management environment. This opens up doors to unmatched composability.
To someone uninitiated, it's unclear what the mirror of assets locked in different chains means, how this "opens up doors to unmatched composability".
There are also a few discrepancies between the code and documentation. For example, the first condition in the rebalancing fee table is incorrect. Although mistakes in inevitable, many mistakes add additional understanding time to auditors and other technical-minded readers.
I also appreciated that there were videos that walked through the code.
One of the ethos I got from this project, is that decentralization is the developer's minds. Although there are a lot of onlyOwner
type of functions, as noted by the Sponsor, a lot of the contracts' ownership can either be renounced as needed, or would be given to Governance.
Anycall is one of the key components of making all this work. There is a lack of integration testing for this component.
Test Coverage
I think the biggest weakness of this codebase was that the coverage was unclear. As mentioned in the discord discussion, forge coverage
does not work. From an auditor perspective, this created a lot of blindspots on what to focus on. As a developer, it's unclear what code remains untested. After reviewing some of the tests, I believe there are missing tests (such as UlyssesFactory). This resulted in many of the issues found.
Another minor nitpick, but related, is that of the reuse of custom errors. Some custom errors, such as DelegationError
is used in multiple functions (see ERC20MultiVotes). This made debugging harder when I was running my own tests.
60 hours
#0 - c4-judge
2023-07-11T08:38:26Z
trust1995 marked the issue as grade-b
#1 - c4-judge
2023-07-11T08:38:32Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T23:15:54Z
0xBugsy marked the issue as sponsor confirmed