Skip to content

Instantly share code, notes, and snippets.

@ChaseTheLight01
Created June 19, 2024 14:08
Show Gist options
  • Save ChaseTheLight01/43d4dbd3aa0ebb1b3531a81bc4a82efd to your computer and use it in GitHub Desktop.
Save ChaseTheLight01/43d4dbd3aa0ebb1b3531a81bc4a82efd to your computer and use it in GitHub Desktop.
LightChaserV3_Cantina_Usual_Money

LightChaser-V3

Generated for: Cantina : Usual Money

Generated on: 2024-06-19

Total findings: 98

Total Medium findings: 7

Total Low findings: 22

Total Gas findings: 32

Total NonCritical findings: 37

Summary for Medium findings

Number Details Instances
[Medium-1] High level access functions can create points of failure (Not captured by 4nalyzer) 3
[Medium-2] deposit/redeem functions found which implement accounting arithmetic vulnerable to the donation attack 2
[Medium-3] Withdrawal mechanism can be Dos'd with dust withdrawals 2
[Medium-4] Privileged functions can create points of failure 7
[Medium-5] Insufficient oracle validation 1
[Medium-6] Chainlink oracle will return the wrong price if the aggregator hits minAnswer 2
[Medium-7] Lack of protection against down Oracles. 2

Summary for Low findings

Number Details Instances
[Low-1] Code does not follow the best practice of check-effects-interaction 4
[Low-2] Avoid making withdraw/unstake functions Pausable 1
[Low-3] Some tokens may revert when zero value transfers are made 5
[Low-4] Solidity version 0.8.20 won't work on all chains due to PUSH0 1
[Low-5] Upgradable contracts should have a __gap variable 8
[Low-6] Contracts are vulnerable to fee-on-transfer accounting-related issues 1
[Low-7] Initializer function can be front run 9
[Low-8] The function decimals() is not part of the ERC20 standard 6
[Low-9] Minting to the zero address should be avoided 1
[Low-10] Loss of precision 1
[Low-11] Off-by-one timestamp error 1
[Low-12] Constant decimal values 1
[Low-13] Chainlink price feed decimals not checked 2
[Low-14] Chainlink answer is not compared against min/max values 2
[Low-15] Missing zero address check in initializer 3
[Low-16] Critical functions should have a timelock 3
[Low-17] Consider implementing two-step procedure for updating protocol addresses 1
[Low-18] Lack of support for rebasing tokens 2
[Low-19] Large transfers may not work with some ERC20 tokens 1
[Low-20] Consider a uptime feed on L2 deployments to prevent issues caused by downtime 10
[Low-21] No equate comparison checks between to and from address parameters 1
[Low-22] Events may be emitted out of order due to code not follow the best practice of check-effects-interaction 3

Summary for NonCritical findings

Number Details Instances
[NonCritical-1] Revert on Transfer to the Zero Address 5
[NonCritical-2] approve will always revert as the IERC20 interface mismatch 1
[NonCritical-3] Use forceApprove in place of approve 2
[NonCritical-4] Contract doesn't follow upgradeable paradigm despite importing Upgradeable OpenZeppelin libraries 1
[NonCritical-5] Events regarding state variable changes should emit the previous state variable value 2
[NonCritical-6] In functions which accept an address as a parameter, there should be a zero address check to prevent bugs 37
[NonCritical-7] Default int values are manually set 1
[NonCritical-8] Revert statements within external and public functions can be used to perform DOS attacks 32
[NonCritical-9] Contract lines should not be longer than 120 characters for readability 19
[NonCritical-10] Not all event definitions are utilizing indexed variables. 11
[NonCritical-11] Contracts should have all public/external functions exposed by interfaces 70
[NonCritical-12] Functions within contracts are not ordered according to the solidity style guide 10
[NonCritical-13] Emits without msg.sender parameter 2
[NonCritical-14] Functions with array parameters should have length checks in place 5
[NonCritical-15] All interfaces used within a project should be imported 3
[NonCritical-16] A function which defines named returns in it's declaration doesn't need to use return 2
[NonCritical-17] Constants should be on the left side of the comparison 19
[NonCritical-18] Initialize functions do not emit an event 9
[NonCritical-19] Both immutable and constant state variables should be CONSTANT_CASE 8
[NonCritical-20] Use of non-named numeric constants 5
[NonCritical-21] Redundant else statement 1
[NonCritical-22] Event emit should emit a parameter 5
[NonCritical-23] Unused structs present 1
[NonCritical-24] Empty bytes check is missing 8
[NonCritical-25] Return bool not explicit 1
[NonCritical-26] Do not use underscore at the end of variable name 2
[NonCritical-27] Cyclomatic complexity in functions 8
[NonCritical-28] Missing events in sensitive functions 4
[NonCritical-29] No limit when setting fees in initializer 1
[NonCritical-30] Pure function storage pointer bug 8
[NonCritical-31] Try catch statement without human readable error 5
[NonCritical-32] Avoid declaring variables with the names of defined functions within the project 6
[NonCritical-33] Constructors should emit an event 4
[NonCritical-34] Events should have parameters 5
[NonCritical-35] Avoid arithmetic directly within array indices 1
[NonCritical-36] Memory-safe annotation missing 1
[NonCritical-37] ERC777 tokens can introduce reentrancy risks 7

Summary for Gas findings

Number Details Instances Gas
[Gas-1] Using constants directly, rather than caching the value, saves gas 8 0.0
[Gas-2] Using named returns for pure and view functions is cheaper than using regular returns 27 18954
[Gas-3] Public functions not used internally can be marked as external to save gas 17 0.0
[Gas-4] Calldata should be used in place of memory function parameters when not mutated 3 117
[Gas-5] Usage of smaller uint/int types causes overhead 26 37180
[Gas-6] Use != 0 instead of > 0 2 12
[Gas-7] Integer increments by one can be unchecked to save on gas fees 2 480
[Gas-8] Default bool values are manually reset 6 0.0
[Gas-9] Default int values are manually reset 1 0.0
[Gas-10] Use assembly to check for the zero address 17 0.0
[Gas-11] Can transfer 0 2 0.0
[Gas-12] Structs can be packed into fewer storage slots 4 40000
[Gas-13] Private functions used once can be inlined 1 0.0
[Gas-14] Use assembly to emit events 24 21888
[Gas-15] Use solady library where possible to save gas 7 49000
[Gas-16] Using private rather than public for constants and immutables, saves gas 8 0.0
[Gas-17] Mark Functions That Revert For Normal Users As payable 7 1225
[Gas-18] Unnecessary casting as variable is already of the same type 1 22
[Gas-19] Simple checks for zero uint can be done using assembly to save gas 1 6
[Gas-20] Using nested if to save gas 5 150
[Gas-21] Using constants directly, rather than caching the value, saves gas 8 0.0
[Gas-22] Inline modifiers used only once 2 0.0
[Gas-23] Internal functions only used once can be inlined to save gas 14 5880
[Gas-24] Constructors can be marked as payable to save deployment gas 4 0.0
[Gas-25] Assigning to structs can be more efficient 1 130
[Gas-26] Internal functions never used once can be removed 1 0.0
[Gas-27] There are comparisons to boolean literals (true and false), these can be simplified to save gas 1 18
[Gas-28] It is a waste of GAS to emit variable literals 1 8
[Gas-29] Use OZ Array.unsafeAccess() to avoid repeated array length checks 2 8400
[Gas-30] Use uint256(1)/uint256(2) instead of true/false to save gas for changes 11 2057000
[Gas-31] Consider pre-calculating the address of address(this) to save gas 11 0.0
[Gas-32] Use constants instead of type(uint).max 2 0.0

[Medium-1] High level access functions can create points of failure (Not captured by 4nalyzer)

Resolution

High-level access functions in software, especially those with administrative privileges, can lead to points of failure due to their potential for misuse and unauthorized access. These functions, if compromised, can allow attackers to escalate privileges, leading to unauthorized control or access to sensitive data and functionalities. Insecure direct object references, where user inputs aren't properly validated, can exacerbate this risk, allowing attackers to manipulate the system. Ensuring robust access control and validation mechanisms is crucial to mitigate these vulnerabilities

Num of instances: 3

Findings

Click to show findings

['14']

14:     function onlyMatchingRole(IRegistryAccess registryAccess, bytes32 role) internal view {
15:         if (!registryAccess.hasRole(role, msg.sender)) { // <= FOUND
16:             revert NotAuthorized();
17:         }
18:     }

['83']

83:     function setContract(bytes32 name, address contractAddress) external {
84:         
85:         if (contractAddress == address(0)) {
86:             revert NullAddress();
87:         }
88:         
89:         if (name == bytes32(0)) {
90:             revert InvalidName();
91:         }
92: 
93:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
94:         
95:         if (!IRegistryAccess($._registryAccess).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { // <= FOUND
96:             revert NotAuthorized();
97:         }
98: 
99:         $._contracts[name] = contractAddress;
100:         emit SetContract(name, contractAddress);
101:     }

['266']

266:     function emergencyWithdraw(address safeAccount) external {
267:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
268: 
269:         if (!$.registryAccess.hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { // <= FOUND
270:             revert NotAuthorized();
271:         }
272:         IERC20 usd0 = $.usd0;
273: 
274:         uint256 balance = usd0.balanceOf(address(this));
275:         
276:         usd0.safeTransfer(safeAccount, balance);
277: 
278:         
279:         _pause();
280: 
281:         emit EmergencyWithdraw(safeAccount, balance);
282:     }

[Medium-2] deposit/redeem functions found which implement accounting arithmetic vulnerable to the donation attack

Resolution

Calculations using non-internal accounting, such as balanceOf or totalSupply, can introduce potential donation attack vectors. For instance, consider a scenario where a reward calculation uses totalSupply during a deposit. An attacker could exploit this by donating a large amount of tokens to the vault before the user tries to redeem their withdrawal. This manipulation causes the totalSupply to change significantly, altering the reward calculations and potentially leading to unexpected or unfair outcomes for users.

Let's say a smart contract calculates user rewards based on their share of the total token supply:

uint256 reward = (userDeposit * totalRewards) / totalSupply;

If an attacker donates a large number of tokens to the vault before users withdraw, totalSupply increases, thereby diluting each user's share of the rewards. This discrepancy between expected and actual rewards can undermine user trust and contract integrity.

The risks associated with this attack include reward manipulation, where attackers can alter totalSupply to reduce the share of honest users, and economic attacks, where significant token donations destabilize the contract's economic assumptions, leading to unfair advantages or financial losses. Additionally, users may find their rewards significantly lower than expected, causing confusion and dissatisfaction.

To prevent such vulnerabilities, it's essential to use internal variables to track deposits, withdrawals, and rewards instead of relying on balanceOf or totalSupply. This approach isolates contract logic from external token movements. Implementing a snapshot mechanism to capture totalSupply at specific points in time ensures consistent reward calculations. Introducing limits on the number of tokens that can be deposited or donated in a single transaction can also prevent significant manipulation.

Num of instances: 2

Findings

Click to show findings

['230']

230:     function unwrap() external nonReentrant whenNotPaused { // <= FOUND
231:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
232: 
233:         
234:         if (block.timestamp < $.bondStart + BOND_DURATION_FOUR_YEAR) {
235:             revert BondNotFinished();
236:         }
237:         uint256 usd0PPBalance = balanceOf(msg.sender);
238: 
239:         _burn(msg.sender, usd0PPBalance);
240: 
241:         $.usd0.safeTransfer(msg.sender, usd0PPBalance);
242: 
243:         emit BondUnwrapped(msg.sender, usd0PPBalance);
244:     }

['266']

266:     function emergencyWithdraw(address safeAccount) external { // <= FOUND
267:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
268: 
269:         if (!$.registryAccess.hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
270:             revert NotAuthorized();
271:         }
272:         IERC20 usd0 = $.usd0;
273: 
274:         uint256 balance = usd0.balanceOf(address(this));
275:         
276:         usd0.safeTransfer(safeAccount, balance);
277: 
278:         
279:         _pause();
280: 
281:         emit EmergencyWithdraw(safeAccount, balance);
282:     }

[Medium-3] Withdrawal mechanism can be Dos'd with dust withdrawals

Resolution

As the 'amount' value is not validated against a minimum amount, a potential attacker can make a large number of micro withdrawal requests such a 1e1 for a token with 1e18 decimals. As such they can prevent other users from withdrawing their assets. A resolution to this is implementing a minimum withdrawal amount, although this will need setter limits of it's own to prevent it from being changed to a number so high, no one can withdraw.

Num of instances: 2

Findings

Click to show findings

['701']

701:     function redeem(address rwaToken, uint256 amount, uint256 minAmountOut)
702:         external
703:         nonReentrant
704:         whenRedeemNotPaused
705:         whenNotPaused
706:     {
707:         
708:         if (amount == 0) {
709:             revert AmountIsZero();
710:         }
711: 
712:         
713:         if (!_daoCollateralStorageV0().tokenMapping.isUsd0Collateral(rwaToken)) {
714:             revert InvalidToken();
715:         }
716:         uint256 stableFee = _transferFee(amount, rwaToken);
717:         uint256 returnedCollateral =
718:             _burnStableTokenAndTransferCollateral(rwaToken, amount, stableFee);
719:         
720:         if (returnedCollateral < minAmountOut) {
721:             revert AmountTooLow();
722:         }
723:         emit Redeem(msg.sender, rwaToken, amount, returnedCollateral, stableFee);
724:     }

['727']

727:     function redeemDao(address rwaToken, uint256 amount) external nonReentrant {
728:         
729:         if (amount == 0) {
730:             revert AmountIsZero();
731:         }
732: 
733:         _requireOnlyAdmin();
734:         
735:         if (!_daoCollateralStorageV0().tokenMapping.isUsd0Collateral(rwaToken)) {
736:             revert InvalidToken();
737:         }
738:         uint256 returnedCollateral = _burnStableTokenAndTransferCollateral(rwaToken, amount, 0);
739:         emit Redeem(msg.sender, rwaToken, amount, returnedCollateral, 0);
740:     }

[Medium-4] Privileged functions can create points of failure

Resolution

Ensure such accounts are protected and consider implementing multi sig to prevent a single point of failure

Num of instances: 7

Findings

Click to show findings

['233']

233:     function _requireOnlyAdmin() internal view  // <= FOUND

['39']

39:     function setRoleAdmin(bytes32 role, bytes32 adminRole) external  // <= FOUND

['233']

233:     function _requireOnlyAdmin() internal view  // <= FOUND

['114']

114:     function setMaxDepegThreshold(uint256 maxAuthorizedDepegPrice) external virtual  // <= FOUND

['14']

14:     function onlyMatchingRole(IRegistryAccess registryAccess, bytes32 role) internal view {
15:         if (!registryAccess.hasRole(role, msg.sender)) { // <= FOUND
16:             revert NotAuthorized(); // <= FOUND
17:         }
18:     }

['83']

83:     function setContract(bytes32 name, address contractAddress) external {
84:         
85:         if (contractAddress == address(0)) {
86:             revert NullAddress();
87:         }
88:         
89:         if (name == bytes32(0)) {
90:             revert InvalidName();
91:         }
92: 
93:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
94:         
95:         if (!IRegistryAccess($._registryAccess).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { // <= FOUND
96:             revert NotAuthorized(); // <= FOUND
97:         }
98: 
99:         $._contracts[name] = contractAddress;
100:         emit SetContract(name, contractAddress);
101:     }

['266']

266:     function emergencyWithdraw(address safeAccount) external {
267:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
268: 
269:         if (!$.registryAccess.hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { // <= FOUND
270:             revert NotAuthorized(); // <= FOUND
271:         }
272:         IERC20 usd0 = $.usd0;
273: 
274:         uint256 balance = usd0.balanceOf(address(this));
275:         
276:         usd0.safeTransfer(safeAccount, balance);
277: 
278:         
279:         _pause();
280: 
281:         emit EmergencyWithdraw(safeAccount, balance);
282:     }

[Medium-5] Insufficient oracle validation

Resolution

The contract currently lacks a mechanism to ensure that prices fetched from the Chainlink oracle are up-to-date. In scenarios where the Off-Chain Reporting (OCR) protocol fails to update prices in a timely manner, stale price data could be used inadvertently. This could potentially lead to incorrect contract operations, including mispriced transactions. To mitigate this, consider incorporating a staleness threshold (defined in seconds) into the contract configuration. This would enforce that any price data used is within this specified freshness timeframe, thereby ensuring the contract only operates with relevant and recent price information.

Num of instances: 1

Findings

Click to show findings

['90']

90:     function _latestRoundData(address token) internal view override returns (uint256, uint256) { // <= FOUND
91:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
92:         IDataPublisher dataPublisher = IDataPublisher($.tokenToOracleInfo[token].dataSource);
93: 
94:         if (address(dataPublisher) == address(0)) revert OracleNotInitialized();
95: 
96:         
97:         (, int256 answer,, uint256 decimals) = dataPublisher.latestRoundData(token); // <= FOUND
98: 
99:         if (answer <= 0) revert OracleNotWorkingNotCurrent();
100: 
101:         return (uint256(answer), decimals);
102:     }

[Medium-6] Chainlink oracle will return the wrong price if the aggregator hits minAnswer

Resolution

Chainlink oracles use data aggregators to provide asset prices, incorporating mechanisms like circuit breakers to handle extreme market conditions. These circuit breakers, including parameters like minAnswer, are designed to prevent erratic data or manipulation. However, if an asset's price plummets rapidly beyond these predefined bands (as seen in the LUNA crash), the oracle might freeze and keep returning the minPrice, not reflecting the asset's actual market value. This discrepancy can lead to incorrect borrowing rates in DeFi platforms, as was observed in the Venus incident on BSC, where users were able to borrow against devalued collateral, leading to significant platform losses. This scenario underscores the importance of robust oracle design and risk management strategies in DeFi protocols.

Num of instances: 2

Findings

Click to show findings

['47']

47:     function initializeTokenOracle(
48:         address token,
49:         address dataSource,
50:         uint64 timeout,
51:         bool isStablecoin
52:     ) external {
53:         if (token == address(0)) revert NullAddress();
54:         if (dataSource == address(0)) revert NullAddress();
55:         
56:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout();
57: 
58:         
59:         (, int256 answer,, uint256 updatedAt,) = IAggregator(dataSource).latestRoundData(); // <= FOUND
60:         if (answer <= 0 || updatedAt == 0 || block.timestamp > updatedAt + timeout) {
61:             revert OracleNotWorkingNotCurrent();
62:         }
63: 
64:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
65:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
66: 
67:         $.tokenToOracleInfo[token].dataSource = dataSource;
68:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
69:         $.tokenToOracleInfo[token].timeout = timeout;
70:     }

['73']

73:     function _latestRoundData(address token) internal view override returns (uint256, uint256) {
74:         AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
75:         IAggregator priceAggregatorProxy = IAggregator($.tokenToOracleInfo[token].dataSource);
76: 
77:         if (address(priceAggregatorProxy) == address(0)) revert OracleNotInitialized();
78: 
79:         uint256 decimals = priceAggregatorProxy.decimals();
80: 
81:         
82:         (, int256 answer,, uint256 updatedAt,) = priceAggregatorProxy.latestRoundData(); // <= FOUND
83:         if (answer <= 0) revert OracleNotWorkingNotCurrent();
84:         if (updatedAt > block.timestamp) revert OracleNotWorkingNotCurrent();
85:         
86:         
87:         
88:         if (block.timestamp > $.tokenToOracleInfo[token].timeout + updatedAt) {
89:             revert OracleNotWorkingNotCurrent();
90:         }
91:         return (uint256(answer), decimals);
92:     }

[Medium-7] Lack of protection against down Oracles.

Resolution

The contract currently lacks a mechanism to ensure that prices fetched from the Chainlink oracle are up-to-date and the oracle is up. In scenarios where the Off-Chain Reporting (OCR) protocol fails to update prices in a timely manner, stale price data could be used inadvertently. This could potentially lead to incorrect contract operations, including mispriced transactions. To mitigate this, consider incorporating a staleness threshold (defined in seconds) into the contract configuration. This would enforce that any price data used is within this specified freshness timeframe, thereby ensuring the contract only operates with relevant and recent price information.

Num of instances: 2

Findings

Click to show findings

['128']

128:     function getPrice(address token) public view override returns (uint256) {
129:         (uint256 price, uint256 decimalsPrice) = _latestRoundData(token); // <= FOUND
130:         price = price.tokenAmountToWad(uint8(decimalsPrice));
131:         _checkDepegPrice(token, price);
132:         return price;
133:     }

['90']

90:     function _latestRoundData(address token) internal view override returns (uint256, uint256) { // <= FOUND
91:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
92:         IDataPublisher dataPublisher = IDataPublisher($.tokenToOracleInfo[token].dataSource);
93: 
94:         if (address(dataPublisher) == address(0)) revert OracleNotInitialized();
95: 
96:         
97:         (, int256 answer,, uint256 decimals) = dataPublisher.latestRoundData(token); // <= FOUND
98: 
99:         if (answer <= 0) revert OracleNotWorkingNotCurrent();
100: 
101:         return (uint256(answer), decimals);
102:     }

[Low-1] Code does not follow the best practice of check-effects-interaction

Resolution

The "check-effects-interaction" pattern is a best practice in smart contract development, emphasizing the order of operations in functions to prevent reentrancy attacks. Violations arise when a function interacts with external contracts before settling internal state changes or checks. This misordering can expose the contract to potential threats. To adhere to this pattern, first ensure all conditions or checks are satisfied, then update any internal states, and only after these steps, interact with external contracts or addresses. Rearranging operations to this recommended sequence bolsters contract security and aligns with established best practices in the Ethereum community.

Num of instances: 4

Findings

Click to show findings

['87']

87:     function __AbstractOracle_init_unchained(address registryContract) internal onlyInitializing { // <= FOUND
88:         if (registryContract == address(0)) {
89:             revert NullAddress();
90:         }
91: 
92:         AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
93:         $.registryContract = IRegistryContract(registryContract);
94:         $.registryAccess = IRegistryAccess($.registryContract.getContract(CONTRACT_REGISTRY_ACCESS)); // <= FOUND
95:         $.maxDepegThreshold = INITIAL_MAX_DEPEG_THRESHOLD;
96:         emit SetMaxDepegThreshold(INITIAL_MAX_DEPEG_THRESHOLD);
97:     }

['124']

124:     function initialize(address registryContract) public initializer { // <= FOUND
125:         if (registryContract == address(0)) {
126:             revert NullContract();
127:         }
128:         __Pausable_init_unchained();
129:         __ReentrancyGuard_init_unchained();
130:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
131:         $.registryContract = IRegistryContract(registryContract);
132:         $.registryAccess = IRegistryAccess($.registryContract.getContract(CONTRACT_REGISTRY_ACCESS)); // <= FOUND
133:         $.usdcToken = IERC20($.registryContract.getContract(CONTRACT_USDC)); // <= FOUND
134:         $.usd0 = IERC20($.registryContract.getContract(CONTRACT_USD0)); // <= FOUND
135:         $.nextOrderId = 1;
136:         $.oracle = IOracle($.registryContract.getContract(CONTRACT_ORACLE)); // <= FOUND
137:         $.minimumUSDCAmountProvided = MINIMUM_USDC_PROVIDED;
138:     }

['47']

47:     function initializeTokenOracle(
48:         address token,
49:         address dataSource,
50:         uint64 timeout,
51:         bool isStablecoin
52:     ) external {
53:         if (token == address(0)) revert NullAddress();
54:         if (dataSource == address(0)) revert NullAddress();
55:         
56:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout();
57: 
58:         
59:         (, int256 answer,, uint256 updatedAt,) = IAggregator(dataSource).latestRoundData(); // <= FOUND
60:         if (answer <= 0 || updatedAt == 0 || block.timestamp > updatedAt + timeout) {
61:             revert OracleNotWorkingNotCurrent();
62:         }
63: 
64:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
65:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
66: 
67:         $.tokenToOracleInfo[token].dataSource = dataSource;
68:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
69:         $.tokenToOracleInfo[token].timeout = timeout;
70:     }

['85']

85:     function addUsd0Rwa(address rwa) external returns (bool) { // <= FOUND
86:         if (rwa == address(0)) {
87:             revert NullAddress();
88:         }
89:         
90:         
91:         
92:         if (IERC20Metadata(rwa).decimals() == 0) { // <= FOUND
93:             revert Invalid();
94:         }
95: 
96:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
97:         $._registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
98: 
99:         
100:         if ($.isUsd0Collateral[rwa]) revert SameValue();
101:         $.isUsd0Collateral[rwa] = true;
102:         
103:         ++$._usd0ToRwaLastId;
104:         if ($._usd0ToRwaLastId > MAX_RWA_COUNT) {
105:             revert TooManyRWA();
106:         }
107:         $.USD0Rwas[$._usd0ToRwaLastId] = rwa;
108:         emit AddUsd0Rwa(rwa, $._usd0ToRwaLastId);
109:         return true;
110:     }

[Low-2] Avoid making withdraw/unstake functions Pausable

Resolution

Making withdraw or unstake functions pausable can create a risk by potentially locking users' funds indefinitely, especially in scenarios where the contract is paused for a prolonged period. This design could undermine trust and may not align with the decentralization principles of blockchain. It's advisable to design these functions with user security and accessibility in mind, ensuring that pausing capabilities are used judiciously and transparently, with clear conditions for resuming normal operations.

Num of instances: 1

Findings

Click to show findings

['281']

281:     function withdrawUSDC(uint256 orderToCancel) external nonReentrant whenNotPaused  // <= FOUND

[Low-3] Some tokens may revert when zero value transfers are made

Resolution

Reason: In Solidity, ERC20 token transfers of value 0 can sometimes lead to unexpected issues. This is particularly relevant when dealing with fractional token amounts that round to 0 when less than 1 of the smallest unit is transferred, leading to an effective transfer of nothing while still consuming gas. Furthermore, some ERC20 token implementations may revert on attempts to transfer a value of 0. However, note that this issue doesn't generally apply to wrapper native tokens like WETH.

Resolution: It's advisable to include a condition before any transfer operation to bypass the transaction if the transfer amount is 0. This saves unnecessary gas expenditure and prevents potential function reverts. For handling fractions, ensure token decimals are appropriately assigned and contemplate setting a minimum transfer threshold to avoid rounding down to 0. When dealing with wrapped tokens like WETH, special consideration should be given to their unique characteristics.

Num of instances: 5

Findings

Click to show findings

['236']

236:     function _depositUSDC(SwapperEngineStorageV0 storage $, uint256 amountToDeposit) internal { // <= FOUND
237:         if (amountToDeposit < $.minimumUSDCAmountProvided) {
238:             
239:             revert AmountTooLow();
240:         }
241:         if (IUsd0(address($.usd0)).isBlacklisted(msg.sender)) {
242:             revert NotAuthorized();
243:         }
244: 
245:         uint256 orderId = $.nextOrderId++;
246:         $.orders[orderId] =
247:             UsdcOrder({requester: msg.sender, tokenAmount: amountToDeposit, active: true});
248: 
249:         
250:         $.usdcToken.safeTransferFrom(msg.sender, address(this), amountToDeposit); // <= FOUND
251: 
252:         emit Deposit(msg.sender, orderId, amountToDeposit);
253:     }

['281']

281:     function withdrawUSDC(uint256 orderToCancel) external nonReentrant whenNotPaused { // <= FOUND
282:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
283:         UsdcOrder storage order = $.orders[orderToCancel];
284: 
285:         if (!order.active) {
286:             
287:             revert OrderNotActive();
288:         }
289:         if (order.requester != msg.sender) {
290:             
291:             revert NotRequester();
292:         }
293: 
294:         uint256 amountToWithdraw = order.tokenAmount;
295:         order.active = false; 
296:         order.tokenAmount = 0; 
297: 
298:         
299:         $.usdcToken.safeTransfer(msg.sender, amountToWithdraw); // <= FOUND
300: 
301:         emit Withdraw(msg.sender, orderToCancel, amountToWithdraw);
302:     }

['169']

169:     function mint(uint256 amountUsd0) public nonReentrant whenNotPaused { // <= FOUND
170:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
171: 
172:         
173:         if (block.timestamp < $.bondStart) {
174:             revert BondNotStarted();
175:         }
176:         
177:         if (block.timestamp >= $.bondStart + BOND_DURATION_FOUR_YEAR) {
178:             revert BondFinished();
179:         }
180: 
181:         
182:         $.usd0.safeTransferFrom(msg.sender, address(this), amountUsd0); // <= FOUND
183: 
184:         
185:         _mint(msg.sender, amountUsd0);
186:     }

['230']

230:     function unwrap() external nonReentrant whenNotPaused { // <= FOUND
231:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
232: 
233:         
234:         if (block.timestamp < $.bondStart + BOND_DURATION_FOUR_YEAR) {
235:             revert BondNotFinished();
236:         }
237:         uint256 usd0PPBalance = balanceOf(msg.sender);
238: 
239:         _burn(msg.sender, usd0PPBalance);
240: 
241:         $.usd0.safeTransfer(msg.sender, usd0PPBalance); // <= FOUND
242: 
243:         emit BondUnwrapped(msg.sender, usd0PPBalance);
244:     }

['266']

266:     function emergencyWithdraw(address safeAccount) external { // <= FOUND
267:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
268: 
269:         if (!$.registryAccess.hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
270:             revert NotAuthorized();
271:         }
272:         IERC20 usd0 = $.usd0;
273: 
274:         uint256 balance = usd0.balanceOf(address(this));
275:         
276:         usd0.safeTransfer(safeAccount, balance); // <= FOUND
277: 
278:         
279:         _pause();
280: 
281:         emit EmergencyWithdraw(safeAccount, balance);
282:     }

[Low-4] Solidity version 0.8.20 won't work on all chains due to PUSH0

Resolution

Solidity version 0.8.20 uses the new Shanghai EVM which introduces the PUSH0 opcode, this may not be implemented on all chains and L2 thus reducing the portability and compatability of the code. Consider using a earlier solidity version.

Num of instances: 1

Findings

Click to show findings

['3']

3: pragma solidity 0.8.20; // <= FOUND

[Low-5] Upgradable contracts should have a __gap variable

Resolution

This is to ensure the team can add new state variables without compromising compatability.

Num of instances: 8

Findings

Click to show findings

['71']

71: contract DaoCollateral is
72:     IDaoCollateral,
73:     Initializable,
74:     ReentrancyGuardUpgradeable,
75:     PausableUpgradeable,
76:     NoncesUpgradeable,
77:     EIP712Upgradeable
78: 

['16']

16: contract RegistryContract is IRegistryContract, Initializable 

['61']

61: contract SwapperEngine is
62:     Initializable,
63:     ReentrancyGuardUpgradeable,
64:     PausableUpgradeable,
65:     ISwapperEngine
66: 

['20']

20: contract TokenMapping is ITokenMapping, Initializable 

['14']

14: contract RegistryAccess is AccessControlDefaultAdminRulesUpgradeable, IRegistryAccess 

['26']

26: contract Usd0 is ERC20PausableUpgradeable, ERC20PermitUpgradeable, IUsd0 

['47']

47: contract Usd0PP is
48:     IUsd0PP,
49:     ERC20PausableUpgradeable,
50:     ERC20PermitUpgradeable,
51:     ReentrancyGuardUpgradeable
52: 

['25']

25: abstract contract AbstractOracle is IOracle, Initializable 

[Low-6] Contracts are vulnerable to fee-on-transfer accounting-related issues

Resolution

The below-listed functions use transferFrom() to move funds from the sender to the recipient but fail to verify if the received token amount matches the transferred amount. This could pose an issue with fee-on-transfer tokens, where the post-transfer balance might be less than anticipated, leading to balance inconsistencies. There might be subsequent checks for a second transfer, but an attacker might exploit leftover funds (such as those accidentally sent by another user) to gain unjustified credit. A practical solution is to gauge the balance prior and post-transfer, and consider the differential as the transferred amount, instead of the predefined amount.

Num of instances: 1

Findings

Click to show findings

['552']

552:     function _burnStableTokenAndTransferCollateral(
553:         address rwaToken,
554:         uint256 stableAmount,
555:         uint256 stableFee
556:     ) internal returns (uint256 returnedCollateral) {
557:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
558:         
559:         uint256 burnedStable = stableAmount - stableFee;
560:         
561:         $.usd0.burnFrom(msg.sender, stableAmount);
562: 
563:         
564:         returnedCollateral = _getTokenAmountForAmountInUSD(burnedStable, rwaToken);
565:         if (returnedCollateral == 0) {
566:             revert AmountTooLow();
567:         }
568: 
569:         
570:         
571:         IERC20Metadata(rwaToken).safeTransferFrom($.treasury, msg.sender, returnedCollateral); // <= FOUND
572:     }

[Low-7] Initializer function can be front run

Resolution

In Solidity contract deployment, not making the initialize() function call atomic with the contract creation can leave a vulnerability window. A malicious actor could exploit this time gap and call initialize() before the intended initialization. This action could disrupt the contract's setup, potentially necessitating a full contract re-deployment to ensure proper initialization. To mitigate such risks, it's advised to use a factory contract. This factory contract can be programmed to deploy and initialize a new contract in a single atomic transaction, closing the window of vulnerability and ensuring correct and secure contract initialization.

Num of instances: 9

Findings

Click to show findings

['124']

124:     function initialize(address registryContract) public initializer  // <= FOUND

['260']

260:     function initialize(address _registryContract, uint256 _redeemFee) public initializer  // <= FOUND

['27']

27:     function initialize(address deployer) public initializer  // <= FOUND

['67']

67:     function initialize(address registryAccess_) public initializer  // <= FOUND

['124']

124:     function initialize(address registryContract) public initializer  // <= FOUND

['74']

74:     function initialize(address registryAccess, address registryContract) public initializer  // <= FOUND

[]

67:     function initialize(address registryContract_, string memory name_, string memory symbol_)
68:         public
69:         initializer
70:     

['108']

108:     function initialize(
109:         address registryContract,
110:         string memory name_,
111:         string memory symbol_,
112:         uint256 startTime
113:     ) public initializer  // <= FOUND

['56']

56:     function initialize(address registryContractAddress) public initializer  // <= FOUND

[Low-8] The function decimals() is not part of the ERC20 standard

Resolution

The decimals() function in an ERC20 token contract is used to specify how many decimal places the token can be divided into. However, it should be used with caution because not all ERC20 token contracts implement decimals(), and the function isn't required by the ERC20 standard. Calling decimals() on a contract that doesn't implement it will result in a runtime error. Moreover, even when implemented, the returned value can be manipulated or artificially set, which may cause unexpected behavior. Therefore, always verify the decimal count independently if precision is crucial for your contract logic. When interacting with other ERC20 tokens, consider implementing safeguards or checks to handle potential errors from calling decimals().

Num of instances: 6

Findings

Click to show findings

['466']

466:         decimals = uint8(IERC20Metadata(rwaToken).decimals()); // <= FOUND

['531']

531:         uint8 tokenDecimals = IERC20Metadata(rwaToken).decimals(); // <= FOUND

['186']

186:         uint8 decimals = IERC20Metadata(address($.usdcToken)).decimals(); // <= FOUND

['92']

92:         
93:         
94:         
95:         if (IERC20Metadata(rwa).decimals() == 0) { // <= FOUND

['53']

53:         uint8 tokenDecimals = uint8(IERC20Metadata(token).decimals()); // <= FOUND

['79']

79:         uint256 decimals = priceAggregatorProxy.decimals(); // <= FOUND

[Low-9] Minting to the zero address should be avoided

Resolution

Minting tokens to the zero address in Solidity is a potential pitfall that should be carefully guarded against. The zero address (0x0) is generally used as a default value and represents an uninitialized or null address. Minting tokens to this address effectively burns them, making them inaccessible and permanently removing them from the total supply. This could lead to unintended token loss if performed accidentally. To prevent this, it's important to include a check in the minting function to ensure that the target address is not the zero address. Using a library like OpenZeppelin's Address can provide utility functions like requireNonZero, which simplifies this check and enhances the security of the minting function.

Num of instances: 1

Findings

Click to show findings

['135']

135:     function mint(address to, uint256 amount) public {
136:         if (amount == 0) {
137:             revert AmountIsZero(); // <= FOUND
138:         }
139: 
140:         Usd0StorageV0 storage $ = _usd0StorageV0(); // <= FOUND
141:         $.registryAccess.onlyMatchingRole(USD0_MINT); // <= FOUND
142:         _mint(to, amount); // <= FOUND
143:     }

[Low-10] Loss of precision

Resolution

Dividing by large numbers in Solidity can cause a loss of precision due to the language's inherent integer division behavior. Solidity does not support floating-point arithmetic, and as a result, division between integers yields an integer result, truncating any fractional part. When dividing by a large number, the resulting value may become significantly smaller, leading to a loss of precision, as the fractional part is discarded.

Num of instances: 1

Findings

Click to show findings

['16']

16:     function tokenAmountToDecimals(uint256 tokenAmount, uint8 tokenDecimals, uint8 targetDecimals)
17:         internal
18:         pure
19:         returns (uint256)
20:     {
21:         if (tokenDecimals < targetDecimals) {
22:             return tokenAmount * (10 ** uint256(targetDecimals - tokenDecimals));
23:         } else if (tokenDecimals > targetDecimals) {
24:             return tokenAmount / (10 ** uint256(tokenDecimals - targetDecimals)); // <= FOUND
25:         } else {
26:             return tokenAmount;
27:         }
28:     }

[Low-11] Off-by-one timestamp error

Resolution

In Solidity, using >= or <= to compare against block.timestamp (alias now) may introduce off-by-one errors due to the fact that block.timestamp is only updated once per block and its value remains constant throughout the block's execution. If an operation happens at the exact second when block.timestamp changes, it could result in unexpected behavior. To avoid this, it's safer to use strict inequality operators (> or <). For instance, if a condition should only be met after a certain time, use block.timestamp > time rather than block.timestamp >= time. This way, potential off-by-one errors due to the exact timing of block mining are mitigated, leading to safer, more predictable contract behavior.

Num of instances: 1

Findings

Click to show findings

['169']

169:     function mint(uint256 amountUsd0) public nonReentrant whenNotPaused { // <= FOUND
170:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
171: 
172:         
173:         if (block.timestamp < $.bondStart) {
174:             revert BondNotStarted();
175:         } // <= FOUND
176:         
177:         if (block.timestamp >= $.bondStart + BOND_DURATION_FOUR_YEAR) {
178:             revert BondFinished();
179:         } // <= FOUND
180: 
181:         
182:         $.usd0.safeTransferFrom(msg.sender, address(this), amountUsd0);
183: 
184:         
185:         _mint(msg.sender, amountUsd0);
186:     } // <= FOUND

[Low-12] Constant decimal values

Resolution

The use of fixed decimal values such as 1e18 or 1e8 in Solidity contracts can lead to inaccuracies, bugs, and vulnerabilities, particularly when interacting with tokens having different decimal configurations. Not all ERC20 tokens follow the standard 18 decimal places, and assumptions about decimal places can lead to miscalculations.

Resolution: Always retrieve and use the decimals() function from the token contract itself when performing calculations involving token amounts. This ensures that your contract correctly handles tokens with any number of decimal places, mitigating the risk of numerical errors or under/overflows that could jeopardize contract integrity and user funds.

Num of instances: 1

Findings

Click to show findings

['30']

30: uint256 constant SCALAR_ONE = 1e18; // <= FOUND

[Low-13] Chainlink price feed decimals not checked

Resolution

Price feeds provided by Chainlink oracles are an essential resource for many DeFi applications in the Ethereum ecosystem, enabling smart contracts to access reliable, tamper-resistant price data. However, these price feeds are often normalized using a certain number of decimal places, a fact which can potentially lead to discrepancies or inaccuracies if not properly accounted for.

For example, if a Chainlink price feed returns a price with 8 decimal places, but your contract assumes the price has 18 decimal places (which is common with Ether and many ERC20 tokens), this could cause an underflow in your contract's calculations, which might lead to incorrect operations or financial discrepancies.

As a best practice, your contract should dynamically check the number of decimals that the Chainlink price feed uses. This information can be retrieved via the decimals() function provided by the Chainlink AggregatorV3Interface. Once you have this information, you can correctly scale the price data to match your contract's assumptions, ensuring that all calculations are accurate and consistent.

This practice not only makes your contract more resilient to potential discrepancies in the data feed but also enhances its interoperability by ensuring it can correctly interact with different types of price feeds, each possibly having different decimal normalization.

Num of instances: 2

Findings

Click to show findings

['47']

47:     function initializeTokenOracle(
48:         address token,
49:         address dataSource,
50:         uint64 timeout,
51:         bool isStablecoin
52:     ) external {
53:         if (token == address(0)) revert NullAddress();
54:         if (dataSource == address(0)) revert NullAddress();
55:         
56:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout();
57: 
58:         
59:         (, int256 answer,, uint256 updatedAt,) = IAggregator(dataSource).latestRoundData(); // <= FOUND
60:         if (answer <= 0 || updatedAt == 0 || block.timestamp > updatedAt + timeout) {
61:             revert OracleNotWorkingNotCurrent();
62:         }
63: 
64:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
65:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
66: 
67:         $.tokenToOracleInfo[token].dataSource = dataSource;
68:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
69:         $.tokenToOracleInfo[token].timeout = timeout;
70:     }

['69']

69:     function initializeTokenOracle(address token, uint64 timeout, bool isStablecoin) external {
70:         if (token == address(0)) revert NullAddress();
71:         
72:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout();
73: 
74:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
75:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
76: 
77:         UsualOracleStorageV0 storage u = _usualOracleStorageV0();
78: 
79:         
80:         (, int256 answer, uint256 timestamp,) = u.dataPublisher.latestRoundData(token); // <= FOUND
81:         if (answer <= 0 || block.timestamp - timestamp > timeout) {
82:             revert OracleNotWorkingNotCurrent();
83:         }
84: 
85:         $.tokenToOracleInfo[token].dataSource = address(u.dataPublisher);
86:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
87:     }

[Low-14] Chainlink answer is not compared against min/max values

Resolution

Chainlink oracle provides reliable, real-time data feeds to smart contracts. However, in order to enhance security and minimize the potential impact of an oracle failure or manipulation, it's a good practice to establish minimum and maximum thresholds for these data inputs. Without this safeguard, an erroneous or maliciously manipulated data point from the oracle could potentially lead to severe consequences in contract behavior. Therefore, the values retrieved from Chainlink's oracle should be cross-verified against preset min/max boundaries to ensure they fall within the expected range. This extra layer of validation adds robustness and reduces the risk of oracle-related issues.

Num of instances: 2

Findings

Click to show findings

['47']

47:     function initializeTokenOracle(
48:         address token,
49:         address dataSource,
50:         uint64 timeout,
51:         bool isStablecoin
52:     ) external {
53:         if (token == address(0)) revert NullAddress();
54:         if (dataSource == address(0)) revert NullAddress();
55:         
56:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout();
57: 
58:         
59:         (, int256 answer,, uint256 updatedAt,) = IAggregator(dataSource).latestRoundData();
60:         if (answer <= 0 || updatedAt == 0 || block.timestamp > updatedAt + timeout) {
61:             revert OracleNotWorkingNotCurrent();
62:         }
63: 
64:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
65:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
66: 
67:         $.tokenToOracleInfo[token].dataSource = dataSource;
68:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
69:         $.tokenToOracleInfo[token].timeout = timeout;
70:     }

['73']

73:     function _latestRoundData(address token) internal view override returns (uint256, uint256) {
74:         AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
75:         IAggregator priceAggregatorProxy = IAggregator($.tokenToOracleInfo[token].dataSource);
76: 
77:         if (address(priceAggregatorProxy) == address(0)) revert OracleNotInitialized();
78: 
79:         uint256 decimals = priceAggregatorProxy.decimals();
80: 
81:         
82:         (, int256 answer,, uint256 updatedAt,) = priceAggregatorProxy.latestRoundData();
83:         if (answer <= 0) revert OracleNotWorkingNotCurrent();
84:         if (updatedAt > block.timestamp) revert OracleNotWorkingNotCurrent();
85:         
86:         
87:         
88:         if (block.timestamp > $.tokenToOracleInfo[token].timeout + updatedAt) {
89:             revert OracleNotWorkingNotCurrent();
90:         }
91:         return (uint256(answer), decimals);
92:     }

[Low-15] Missing zero address check in initializer

Resolution

Initializer functions in contracts often set important parameters or addresses. Failing to check for the zero address (0x0000000000000000000000000000000000000000) in initializers can lead to unintended behavior, as this address typically signifies an unset or default value. Transfers to or interactions with the zero address can result in permanent loss of assets or broken functionality. It's crucial to add checks using require(targetAddress != address(0), "Address cannot be zero") in initializers to prevent accidentally setting important state variables or parameters to this address, ensuring the system's integrity and user asset safety.

Num of instances: 3

Findings

Click to show findings

['38']

38:     function initialize(address registryContract) public initializer {
39:         __AbstractOracle_init_unchained(registryContract);
40:     }

['108']

108:     function initialize(
109:         address registryContract,
110:         string memory name_,
111:         string memory symbol_,
112:         uint256 startTime
113:     ) public initializer {
114:         _createUsd0PPCheck(name_, symbol_, startTime);
115: 
116:         __ERC20_init_unchained(name_, symbol_);
117:         __ERC20Permit_init_unchained(name_);
118:         __EIP712_init_unchained(name_, "1");
119:         __ReentrancyGuard_init_unchained();
120:         
121:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
122:         $.bondStart = startTime;
123:         $.registryContract = IRegistryContract(registryContract);
124:         $.usd0 = IERC20(IRegistryContract(registryContract).getContract(CONTRACT_USD0));
125:         $.registryAccess = IRegistryAccess(
126:             IRegistryContract(registryContract).getContract(CONTRACT_REGISTRY_ACCESS)
127:         );
128:     }

['56']

56:     function initialize(address registryContractAddress) public initializer {
57:         __AbstractOracle_init_unchained(registryContractAddress);
58: 
59:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
60:         UsualOracleStorageV0 storage u = _usualOracleStorageV0();
61:         u.dataPublisher = IDataPublisher($.registryContract.getContract(CONTRACT_DATA_PUBLISHER));
62:     }

[Low-16] Critical functions should have a timelock

Resolution

Critical functions, especially those affecting protocol parameters or user funds, are potential points of failure or exploitation. To mitigate risks, incorporating a timelock on such functions can be beneficial. A timelock requires a waiting period between the time an action is initiated and when it's executed, giving stakeholders time to react, potentially vetoing malicious or erroneous changes. To implement, integrate a smart contract like OpenZeppelin's TimelockController or build a custom mechanism. This ensures governance decisions or administrative changes are transparent and allows for community or multi-signature interventions, enhancing protocol security and trustworthiness.

Num of instances: 3

Findings

Click to show findings

['39']

39:     function setRoleAdmin(bytes32 role, bytes32 adminRole) external  // <= FOUND

['114']

114:     function setMaxDepegThreshold(uint256 maxAuthorizedDepegPrice) external virtual  // <= FOUND

['83']

83:     function setContract(bytes32 name, address contractAddress) external  // <= FOUND

[Low-17] Consider implementing two-step procedure for updating protocol addresses

Resolution

Implementing a two-step procedure for updating protocol addresses adds an extra layer of security. In such a system, the first step initiates the change, and the second step, after a predefined delay, confirms and finalizes it. This delay allows stakeholders or monitoring tools to observe and react to unintended or malicious changes. If an unauthorized change is detected, corrective actions can be taken before the change is finalized. To achieve this, introduce a "proposed address" state variable and a "delay period". Upon an update request, set the "proposed address". After the delay, if not contested, the main protocol address can be updated.

Num of instances: 1

Findings

Click to show findings

['83']

83:     function setContract(bytes32 name, address contractAddress) external { // <= FOUND
84:         
85:         if (contractAddress == address(0)) {
86:             revert NullAddress();
87:         }
88:         
89:         if (name == bytes32(0)) {
90:             revert InvalidName();
91:         }
92: 
93:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
94:         
95:         if (!IRegistryAccess($._registryAccess).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
96:             revert NotAuthorized();
97:         }
98: 
99:         $._contracts[name] = contractAddress;
100:         emit SetContract(name, contractAddress);
101:     }

[Low-18] Lack of support for rebasing tokens

Resolution

Rebasing tokens, such as Aave's Tokens, dynamically adjust their holders' balances over time, meaning that the token balance in a user's account increases periodically. This rebase feature poses a challenge when these tokens are held in contracts since any rewards generated accrue directly to the contract, preventing the original depositor from withdrawing them.\n\nTo solve this issue, a viable approach is to maintain a system of 'shares' corresponding to each deposit. These shares denote the proportion of the total contract balance attributable to each depositor. When a withdrawal is initiated, these shares can be redeemed, ensuring each user receives their fair portion of the current balance at the time of withdrawal. This strategy effectively enables the original depositor to benefit from the accrued rewards, despite the rebasing nature of the tokens.

Num of instances: 2

Findings

Click to show findings

['552']

552:     function _burnStableTokenAndTransferCollateral(
553:         address rwaToken,
554:         uint256 stableAmount,
555:         uint256 stableFee
556:     ) internal returns (uint256 returnedCollateral) {
557:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
558:         
559:         uint256 burnedStable = stableAmount - stableFee;
560:         
561:         $.usd0.burnFrom(msg.sender, stableAmount);
562: 
563:         
564:         returnedCollateral = _getTokenAmountForAmountInUSD(burnedStable, rwaToken);
565:         if (returnedCollateral == 0) {
566:             revert AmountTooLow();
567:         }
568: 
569:         
570:         
571:         IERC20Metadata(rwaToken).safeTransferFrom($.treasury, msg.sender, returnedCollateral); // <= FOUND
572:     }

['587']

587:     function _swapRWAtoStbc(
588:         address caller,
589:         address rwaToken,
590:         uint256 amountInTokenDecimals,
591:         bool partialMatching,
592:         uint256[] calldata orderIdsToTake,
593:         Approval calldata approval
594:     ) internal returns (uint256 matchedAmountInTokenDecimals, uint256 matchedAmountInUSD) {
595:         if (amountInTokenDecimals == 0) {
596:             revert AmountIsZero();
597:         }
598:         if (amountInTokenDecimals > type(uint128).max) {
599:             revert AmountTooBig();
600:         }
601:         if (orderIdsToTake.length == 0) {
602:             revert NoOrdersIdsProvided();
603:         }
604:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
605:         if (!$.tokenMapping.isUsd0Collateral(rwaToken)) {
606:             revert InvalidToken();
607:         }
608: 
609:         
610:         if (approval.deadline != 0 && approval.v != 0 && approval.r != 0 && approval.s != 0) {
611:             
612:             try IERC20Permit(rwaToken).permit(
613:                 caller,
614:                 address(this),
615:                 amountInTokenDecimals,
616:                 approval.deadline,
617:                 approval.v,
618:                 approval.r,
619:                 approval.s
620:             ) {} catch {} 
621:         }
622: 
623:         
624:         IERC20Metadata(rwaToken).safeTransferFrom(caller, address(this), amountInTokenDecimals); // <= FOUND
625:         
626:         uint256 wadRwaQuoteInUSD = _getQuoteInUsd(amountInTokenDecimals, rwaToken);
627:         
628:         $.usd0.mint(address(this), wadRwaQuoteInUSD);
629:         if (!IERC20($.usd0).approve(address($.swapperEngine), wadRwaQuoteInUSD)) {
630:             revert ApprovalFailed();
631:         }
632:         
633:         uint256 wadRwaNotTakenInUSD =
634:             $.swapperEngine.swapUsd0(caller, wadRwaQuoteInUSD, orderIdsToTake, partialMatching);
635: 
636:         
637:         if (wadRwaNotTakenInUSD > 0) {
638:             if (!IERC20($.usd0).approve(address($.swapperEngine), 0)) {
639:                 revert ApprovalFailed();
640:             }
641:             $.usd0.burnFrom(address(this), wadRwaNotTakenInUSD);
642: 
643:             
644:             uint256 rwaTokensToReturn = _getQuoteInToken(wadRwaNotTakenInUSD, rwaToken);
645: 
646:             
647:             IERC20Metadata(rwaToken).safeTransfer(caller, rwaTokensToReturn); // <= FOUND
648: 
649:             matchedAmountInTokenDecimals = amountInTokenDecimals - rwaTokensToReturn;
650:         } else {
651:             matchedAmountInTokenDecimals = amountInTokenDecimals;
652:         }
653: 
654:         
655:         IERC20Metadata(rwaToken).safeTransfer($.treasury, matchedAmountInTokenDecimals); // <= FOUND
656: 
657:         matchedAmountInUSD = wadRwaQuoteInUSD - wadRwaNotTakenInUSD;
658:         emit Swap(caller, rwaToken, matchedAmountInTokenDecimals, matchedAmountInUSD);
659: 
660:         return (matchedAmountInTokenDecimals, matchedAmountInUSD);
661:     }

[Low-19] Large transfers may not work with some ERC20 tokens

Resolution

Large transfers with some ERC20 tokens may not work due to various reasons. Some tokens may have transfer restrictions built into the contract, such as daily transfer limits or maximum transfer sizes per transaction, to comply with regulatory requirements or to mitigate risks. Others may face issues with rounding errors when dealing with large quantities, especially if they have a high number of decimal places. Resolution involves carefully reading the token's contract to understand its constraints and behaviors and performing transfers accordingly. It may also be necessary to split large transfers into smaller increments if the token enforces specific transfer limits.

Num of instances: 1

Findings

Click to show findings

['552']

552:     function _burnStableTokenAndTransferCollateral(
553:         address rwaToken,
554:         uint256 stableAmount,
555:         uint256 stableFee
556:     ) internal returns (uint256 returnedCollateral) {
557:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
558:         
559:         uint256 burnedStable = stableAmount - stableFee;
560:         
561:         $.usd0.burnFrom(msg.sender, stableAmount);
562: 
563:         
564:         returnedCollateral = _getTokenAmountForAmountInUSD(burnedStable, rwaToken);
565:         if (returnedCollateral == 0) {
566:             revert AmountTooLow();
567:         }
568: 
569:         
570:         
571:         IERC20Metadata(rwaToken).safeTransferFrom($.treasury, msg.sender, returnedCollateral); // <= FOUND
572:     }

[Low-20] Consider a uptime feed on L2 deployments to prevent issues caused by downtime

Resolution

In L2 deployments, incorporating an uptime feed is crucial to mitigate issues arising from sequencer downtime. Downtime can disrupt services, leading to transaction failures or incorrect data readings, affecting overall system reliability. By integrating an uptime feed, you gain insight into the operational status of the L2 network, enabling proactive measures like halting sensitive operations or alerting users. This approach ensures that your contract behaves predictably and securely during network outages, enhancing the robustness and reliability of your decentralized application, which is especially important in mission-critical or high-stakes environments.

Num of instances: 10

Findings

Click to show findings

['23']

23: contract ClassicalOracle is IOracle, AbstractOracle  // <= FOUND

['71']

71: contract DaoCollateral is // <= FOUND
72:     IDaoCollateral,
73:     Initializable,
74:     ReentrancyGuardUpgradeable,
75:     PausableUpgradeable,
76:     NoncesUpgradeable,
77:     EIP712Upgradeable
78: 

['14']

14: contract RegistryAccess is AccessControlDefaultAdminRulesUpgradeable, IRegistryAccess  // <= FOUND

['16']

16: contract RegistryContract is IRegistryContract, Initializable  // <= FOUND

['61']

61: contract SwapperEngine is // <= FOUND
62:     Initializable,
63:     ReentrancyGuardUpgradeable,
64:     PausableUpgradeable,
65:     ISwapperEngine
66: 

['20']

20: contract TokenMapping is ITokenMapping, Initializable  // <= FOUND

['26']

26: contract Usd0 is ERC20PausableUpgradeable, ERC20PermitUpgradeable, IUsd0  // <= FOUND

['47']

47: contract Usd0PP is // <= FOUND
48:     IUsd0PP,
49:     ERC20PausableUpgradeable,
50:     ERC20PermitUpgradeable,
51:     ReentrancyGuardUpgradeable
52: 

['22']

22: contract UsualOracle is AbstractOracle  // <= FOUND

['25']

25: abstract contract AbstractOracle is IOracle, Initializable  // <= FOUND

[Low-21] No equate comparison checks between to and from address parameters

Resolution

The function lacks a standard check: it does not validate if the 'to' and 'from' addresses are identical. This omission can lead to unintended outcomes if the same address is used in both parameters. To rectify this, we recommend implementing a comparison check at the beginning of the function. In the context of Solidity, the command require(to != from, "To and From addresses can't be the same"); could be utilized. This addition will generate an error if the 'to' and 'from' addresses are the same, thereby fortifying the function's robustness and security.

Num of instances: 1

Findings

Click to show findings

['166']

166:     function _update(address from, address to, uint256 amount) // <= FOUND
167:         internal
168:         virtual
169:         override(ERC20PausableUpgradeable, ERC20Upgradeable)
170:     {
171:         Usd0StorageV0 storage $ = _usd0StorageV0();
172:         if ($.isBlacklisted[from] || $.isBlacklisted[to]) {
173:             revert Blacklisted();
174:         }
175:         super._update(from, to, amount);
176:     }

[Low-22] Events may be emitted out of order due to code not follow the best practice of check-effects-interaction

Resolution

The "check-effects-interaction" pattern also impacts event ordering. When a contract doesn't adhere to this pattern, events might be emitted in a sequence that doesn't reflect the actual logical flow of operations. This can cause confusion during event tracking, potentially leading to erroneous off-chain interpretations. To rectify this, always ensure that checks are performed first, state modifications come next, and interactions with external contracts or addresses are done last. This will ensure events are emitted in a logical, consistent manner, providing a clear and accurate chronological record of on-chain actions for off-chain systems and observers.

Num of instances: 3

Findings

Click to show findings

['87']

87:     function __AbstractOracle_init_unchained(address registryContract) internal onlyInitializing { // <= FOUND
88:         if (registryContract == address(0)) {
89:             revert NullAddress();
90:         }
91: 
92:         AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
93:         $.registryContract = IRegistryContract(registryContract);
94:         $.registryAccess = IRegistryAccess($.registryContract.getContract(CONTRACT_REGISTRY_ACCESS)); // <= FOUND
95:         $.maxDepegThreshold = INITIAL_MAX_DEPEG_THRESHOLD;
96:         emit SetMaxDepegThreshold(INITIAL_MAX_DEPEG_THRESHOLD); // <= FOUND
97:     }

['587']

587:     function _swapRWAtoStbc(
588:         address caller,
589:         address rwaToken,
590:         uint256 amountInTokenDecimals,
591:         bool partialMatching,
592:         uint256[] calldata orderIdsToTake,
593:         Approval calldata approval
594:     ) internal returns (uint256 matchedAmountInTokenDecimals, uint256 matchedAmountInUSD) {
595:         if (amountInTokenDecimals == 0) {
596:             revert AmountIsZero();
597:         }
598:         if (amountInTokenDecimals > type(uint128).max) {
599:             revert AmountTooBig();
600:         }
601:         if (orderIdsToTake.length == 0) {
602:             revert NoOrdersIdsProvided();
603:         }
604:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
605:         if (!$.tokenMapping.isUsd0Collateral(rwaToken)) {
606:             revert InvalidToken();
607:         }
608: 
609:         
610:         if (approval.deadline != 0 && approval.v != 0 && approval.r != 0 && approval.s != 0) {
611:             
612:             try IERC20Permit(rwaToken).permit( // <= FOUND
613:                 caller,
614:                 address(this),
615:                 amountInTokenDecimals,
616:                 approval.deadline,
617:                 approval.v,
618:                 approval.r,
619:                 approval.s
620:             ) {} catch {} 
621:         }
622: 
623:         
624:         IERC20Metadata(rwaToken).safeTransferFrom(caller, address(this), amountInTokenDecimals); // <= FOUND
625:         
626:         uint256 wadRwaQuoteInUSD = _getQuoteInUsd(amountInTokenDecimals, rwaToken);
627:         
628:         $.usd0.mint(address(this), wadRwaQuoteInUSD);
629:         if (!IERC20($.usd0).approve(address($.swapperEngine), wadRwaQuoteInUSD)) {
630:             revert ApprovalFailed();
631:         }
632:         
633:         uint256 wadRwaNotTakenInUSD =
634:             $.swapperEngine.swapUsd0(caller, wadRwaQuoteInUSD, orderIdsToTake, partialMatching);
635: 
636:         
637:         if (wadRwaNotTakenInUSD > 0) {
638:             if (!IERC20($.usd0).approve(address($.swapperEngine), 0)) {
639:                 revert ApprovalFailed();
640:             }
641:             $.usd0.burnFrom(address(this), wadRwaNotTakenInUSD);
642: 
643:             
644:             uint256 rwaTokensToReturn = _getQuoteInToken(wadRwaNotTakenInUSD, rwaToken);
645: 
646:             
647:             IERC20Metadata(rwaToken).safeTransfer(caller, rwaTokensToReturn); // <= FOUND
648: 
649:             matchedAmountInTokenDecimals = amountInTokenDecimals - rwaTokensToReturn;
650:         } else {
651:             matchedAmountInTokenDecimals = amountInTokenDecimals;
652:         }
653: 
654:         
655:         IERC20Metadata(rwaToken).safeTransfer($.treasury, matchedAmountInTokenDecimals); // <= FOUND
656: 
657:         matchedAmountInUSD = wadRwaQuoteInUSD - wadRwaNotTakenInUSD;
658:         emit Swap(caller, rwaToken, matchedAmountInTokenDecimals, matchedAmountInUSD); // <= FOUND
659: 
660:         return (matchedAmountInTokenDecimals, matchedAmountInUSD);
661:     }

['85']

85:     function addUsd0Rwa(address rwa) external returns (bool) { // <= FOUND
86:         if (rwa == address(0)) {
87:             revert NullAddress();
88:         }
89:         
90:         
91:         
92:         if (IERC20Metadata(rwa).decimals() == 0) { // <= FOUND
93:             revert Invalid();
94:         }
95: 
96:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
97:         $._registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
98: 
99:         
100:         if ($.isUsd0Collateral[rwa]) revert SameValue();
101:         $.isUsd0Collateral[rwa] = true;
102:         
103:         ++$._usd0ToRwaLastId;
104:         if ($._usd0ToRwaLastId > MAX_RWA_COUNT) {
105:             revert TooManyRWA();
106:         }
107:         $.USD0Rwas[$._usd0ToRwaLastId] = rwa;
108:         emit AddUsd0Rwa(rwa, $._usd0ToRwaLastId); // <= FOUND
109:         return true;
110:     }

[NonCritical-1] Revert on Transfer to the Zero Address

Resolution

Many ERC-20 and ERC-721 token contracts implement a safeguard that reverts transactions which attempt to transfer tokens to the zero address. This is because such transfers are often the result of programming errors. The OpenZeppelin library, a popular choice for implementing these standards, includes this safeguard. For token contract developers who want to avoid unintentional transfers to the zero address, it's good practice to include a condition that reverts the transaction if the recipient's address is the zero address.

Num of instances: 5

Findings

Click to show findings

['126']

126:     function transferFrom(address sender, address to, uint256 amount) // <= FOUND
127:         public
128:         override(ERC20Upgradeable, IERC20)
129:         returns (bool)
130:     {
131:         return super.transferFrom(sender, to, amount); // <= FOUND
132:     }

['289']

289:     function transferFrom(address sender, address recipient, uint256 amount) // <= FOUND
290:         public
291:         override(ERC20Upgradeable, IERC20)
292:         returns (bool)
293:     {
294:         return super.transferFrom(sender, recipient, amount); // <= FOUND
295:     }

['112']

112:     function transfer(address to, uint256 amount)
113:         public
114:         override(ERC20Upgradeable, IERC20)
115:         returns (bool)
116:     {
117:         return super.transfer(to, amount); // <= FOUND
118:     }

['221']

221:     function transfer(address recipient, uint256 amount)
222:         public
223:         override(ERC20Upgradeable, IERC20)
224:         returns (bool)
225:     {
226:         return super.transfer(recipient, amount); // <= FOUND
227:     }

['266']

266:     function emergencyWithdraw(address safeAccount) external {
267:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
268: 
269:         if (!$.registryAccess.hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
270:             revert NotAuthorized();
271:         }
272:         IERC20 usd0 = $.usd0;
273: 
274:         uint256 balance = usd0.balanceOf(address(this));
275:         
276:         usd0.safeTransfer(safeAccount, balance); // <= FOUND
277: 
278:         
279:         _pause();
280: 
281:         emit EmergencyWithdraw(safeAccount, balance);
282:     }

[NonCritical-2] approve will always revert as the IERC20 interface mismatch

Resolution

In Solidity, using the ERC20 approve function can be problematic with tokens like USDT, which may not fully adhere to the standard interface, potentially causing transaction reverts. To avoid issues, it’s crucial to interact directly with the token's specific ABI rather than the generic IERC20 interface. Before integrating any token, thoroughly review its contract to ensure compatibility, especially for the transfer and approve methods.

Num of instances: 1

Findings

Click to show findings

['587']

587:     function _swapRWAtoStbc(
588:         address caller,
589:         address rwaToken,
590:         uint256 amountInTokenDecimals,
591:         bool partialMatching,
592:         uint256[] calldata orderIdsToTake,
593:         Approval calldata approval
594:     ) internal returns (uint256 matchedAmountInTokenDecimals, uint256 matchedAmountInUSD) {
595:         if (amountInTokenDecimals == 0) {
596:             revert AmountIsZero();
597:         }
598:         if (amountInTokenDecimals > type(uint128).max) {
599:             revert AmountTooBig();
600:         }
601:         if (orderIdsToTake.length == 0) {
602:             revert NoOrdersIdsProvided();
603:         }
604:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
605:         if (!$.tokenMapping.isUsd0Collateral(rwaToken)) {
606:             revert InvalidToken();
607:         }
608: 
609:         
610:         if (approval.deadline != 0 && approval.v != 0 && approval.r != 0 && approval.s != 0) {
611:             
612:             try IERC20Permit(rwaToken).permit(
613:                 caller,
614:                 address(this),
615:                 amountInTokenDecimals,
616:                 approval.deadline,
617:                 approval.v,
618:                 approval.r,
619:                 approval.s
620:             ) {} catch {} 
621:         }
622: 
623:         
624:         IERC20Metadata(rwaToken).safeTransferFrom(caller, address(this), amountInTokenDecimals);
625:         
626:         uint256 wadRwaQuoteInUSD = _getQuoteInUsd(amountInTokenDecimals, rwaToken);
627:         
628:         $.usd0.mint(address(this), wadRwaQuoteInUSD);
629:         if (!IERC20($.usd0).approve(address($.swapperEngine), wadRwaQuoteInUSD)) { // <= FOUND
630:             revert ApprovalFailed();
631:         }
632:         
633:         uint256 wadRwaNotTakenInUSD =
634:             $.swapperEngine.swapUsd0(caller, wadRwaQuoteInUSD, orderIdsToTake, partialMatching);
635: 
636:         
637:         if (wadRwaNotTakenInUSD > 0) {
638:             if (!IERC20($.usd0).approve(address($.swapperEngine), 0)) { // <= FOUND
639:                 revert ApprovalFailed();
640:             }
641:             $.usd0.burnFrom(address(this), wadRwaNotTakenInUSD);
642: 
643:             
644:             uint256 rwaTokensToReturn = _getQuoteInToken(wadRwaNotTakenInUSD, rwaToken);
645: 
646:             
647:             IERC20Metadata(rwaToken).safeTransfer(caller, rwaTokensToReturn);
648: 
649:             matchedAmountInTokenDecimals = amountInTokenDecimals - rwaTokensToReturn;
650:         } else {
651:             matchedAmountInTokenDecimals = amountInTokenDecimals;
652:         }
653: 
654:         
655:         IERC20Metadata(rwaToken).safeTransfer($.treasury, matchedAmountInTokenDecimals);
656: 
657:         matchedAmountInUSD = wadRwaQuoteInUSD - wadRwaNotTakenInUSD;
658:         emit Swap(caller, rwaToken, matchedAmountInTokenDecimals, matchedAmountInUSD);
659: 
660:         return (matchedAmountInTokenDecimals, matchedAmountInUSD);
661:     }

[NonCritical-3] Use forceApprove in place of approve

Resolution

The forceApprove function is a specialized solution addressing the issues that arise with non-standard ERC-20 tokens, such as USDT, which exhibit non-standard behavior in their approve function. These tokens may necessitate setting the allowance to 0 before changing it, may not return a boolean value from approve calls, or may return false instead of reverting on failure. OpenZeppelin's SafeERC20 library includes a forceApprove method to safely handle these peculiarities, ensuring that smart contracts can interact with such tokens without transaction failures. It rectifies inconsistencies by ensuring that all token interactions, including approvals, adhere to expected standards, even when the underlying tokens do not.

Num of instances: 2

Findings

Click to show findings

['629']

629:         if (!IERC20($.usd0).approve(address($.swapperEngine), wadRwaQuoteInUSD)) { // <= FOUND

['638']

638:             if (!IERC20($.usd0).approve(address($.swapperEngine), 0)) { // <= FOUND

[NonCritical-4] Contract doesn't follow upgradeable paradigm despite importing Upgradeable OpenZeppelin libraries

Resolution

The following contracts do not implement upgradeable OpenZeppelin library variants despite the solidity file importing them. This has been flagged to help detect wrong library variants being used accidentally.

Num of instances: 1

Findings

Click to show findings

['16']

16: contract RegistryContract is IRegistryContract, Initializable 

[NonCritical-5] Events regarding state variable changes should emit the previous state variable value

Resolution

Modify such events to contain the previous value of the state variable as demonstrated in the example below

Num of instances: 2

Findings

Click to show findings

['53']

53: event SetContract(bytes32 indexed name, address indexed contractAddress);

['165']

165: event RedeemFeeUpdated(uint256 redeemFee);

[NonCritical-6] In functions which accept an address as a parameter, there should be a zero address check to prevent bugs

Resolution

In smart contract development, especially with Solidity, it's crucial to validate inputs to functions. When a function accepts an Ethereum address as a parameter, implementing a zero address check (i.e., ensuring the address is not 0x0) is a best practice to prevent potential bugs and vulnerabilities. The zero address (0x0) is a default value and generally indicates an uninitialized or invalid state. Passing the zero address to certain functions can lead to unintended behaviors, like funds getting locked permanently or transactions failing silently. By checking for and rejecting the zero address, developers can ensure that the function operates as intended and interacts only with valid Ethereum addresses. This check enhances the contract's robustness and security.

Num of instances: 37

Findings

Click to show findings

['128']

128:     function getPrice(address token) public view override returns (uint256) 

['136']

136:     function getQuote(address token, uint256 amount) external view override returns (uint256) 

['159']

159:     function _checkDepegPrice(address token, uint256 wadPriceInUSD) internal view 

['124']

124:     function initialize(address registryContract) public initializer 

['413']

413:     function _swapCheckAndGetUSDQuote(address rwaToken, uint256 amountInToken)
414:         internal
415:         view
416:         returns (uint256 wadQuoteInUSD)
417:     

['443']

443:     function _transferRWATokenAndMintStable(
444:         address rwaToken,
445:         uint256 amount,
446:         uint256 wadAmountInUSD
447:     ) internal 

['459']

459:     function _getPriceAndDecimals(address rwaToken)
460:         internal
461:         view
462:         returns (uint256 wadPriceInUSD, uint8 decimals)
463:     

['474']

474:     function _getQuoteInUsd(uint256 tokenAmount, address rwaToken)
475:         internal
476:         view
477:         returns (uint256 wadAmountInUSD)
478:     

['489']

489:     function _getQuoteInToken(uint256 wadStableAmount, address rwaToken)
490:         internal
491:         view
492:         returns (uint256 amountInToken)
493:     

['504']

504:     function _getTokenAmountForAmountInUSD(uint256 wadStableAmount, address rwaToken)
505:         internal
506:         view
507:         returns (uint256 amountInToken)
508:     

['525']

525:     function _transferFee(uint256 usd0Amount, address rwaToken)
526:         internal
527:         returns (uint256 stableFee)
528:     

['552']

552:     function _burnStableTokenAndTransferCollateral(
553:         address rwaToken,
554:         uint256 stableAmount,
555:         uint256 stableFee
556:     ) internal returns (uint256 returnedCollateral) 

['587']

587:     function _swapRWAtoStbc(
588:         address caller,
589:         address rwaToken,
590:         uint256 amountInTokenDecimals,
591:         bool partialMatching,
592:         uint256[] calldata orderIdsToTake,
593:         Approval calldata approval
594:     ) internal returns (uint256 matchedAmountInTokenDecimals, uint256 matchedAmountInUSD) 

['668']

668:     function swap(address rwaToken, uint256 amount, uint256 minAmountOut)
669:         public
670:         nonReentrant
671:         whenSwapNotPaused
672:         whenNotPaused
673:     

['685']

685:     function swapWithPermit(
686:         address rwaToken,
687:         uint256 amount,
688:         uint256 minAmountOut,
689:         uint256 deadline,
690:         uint8 v,
691:         bytes32 r,
692:         bytes32 s
693:     ) external 

['701']

701:     function redeem(address rwaToken, uint256 amount, uint256 minAmountOut)
702:         external
703:         nonReentrant
704:         whenRedeemNotPaused
705:         whenNotPaused
706:     

['727']

727:     function redeemDao(address rwaToken, uint256 amount) external nonReentrant 

['743']

743:     function swapRWAtoStbc(
744:         address rwaToken,
745:         uint256 amountInTokenDecimals,
746:         bool partialMatching,
747:         uint256[] calldata orderIdsToTake,
748:         Approval calldata approval
749:     ) external nonReentrant whenNotPaused whenSwapNotPaused 

['313']

313:     function _provideUsd0ReceiveUSDC( 
314:         address recipient,
315:         uint256 amountUsdcToTakeInNativeDecimals,
316:         uint256[] memory orderIdsToTake,
317:         bool partialMatchingAllowed,
318:         uint256 usdcWadPrice
319:     ) internal returns (uint256 unmatchedUsdcAmount, uint256 totalUsd0Provided) 

['384']

384:     function provideUsd0ReceiveUSDC(
385:         address recipient,
386:         uint256 amountUsdcToTakeInNativeDecimals,
387:         uint256[] memory orderIdsToTake,
388:         bool partialMatchingAllowed
389:     ) external nonReentrant whenNotPaused returns (uint256) 

['408']

408:     function provideUsd0ReceiveUSDCWithPermit(
409:         address recipient,
410:         uint256 amountUsdcToTakeInNativeDecimals,
411:         uint256[] memory orderIdsToTake,
412:         bool partialMatchingAllowed,
413:         uint256 usd0ToPermit,
414:         uint256 deadline,
415:         uint8 v,
416:         bytes32 r,
417:         bytes32 s
418:     ) external nonReentrant whenNotPaused returns (uint256) 

['442']

442:     function swapUsd0(
443:         address recipient,
444:         uint256 amountUsd0ToProvideInWad,
445:         uint256[] memory orderIdsToTake,
446:         bool partialMatchingAllowed
447:     ) external nonReentrant whenNotPaused returns (uint256) 

['148']

148:     function isUsd0Collateral(address rwa) external view returns (bool) 

['112']

112:     function transfer(address to, uint256 amount)
113:         public
114:         override(ERC20Upgradeable, IERC20)
115:         returns (bool)
116:     

['126']

126:     function transferFrom(address sender, address to, uint256 amount)
127:         public
128:         override(ERC20Upgradeable, IERC20)
129:         returns (bool)
130:     

['135']

135:     function mint(address to, uint256 amount) public 

['146']

146:     function burnFrom(address account, uint256 amount) public 

['166']

166:     function _update(address from, address to, uint256 amount)
167:         internal
168:         virtual
169:         override(ERC20PausableUpgradeable, ERC20Upgradeable)
170:     

['198']

198:     function unBlacklist(address account) external 

['210']

210:     function isBlacklisted(address account) external view returns (bool) 

['108']

108:     function initialize(
109:         address registryContract,
110:         string memory name_,
111:         string memory symbol_,
112:         uint256 startTime
113:     ) public initializer 

['201']

201:     function _update(address sender, address recipient, uint256 amount)
202:         internal
203:         override(ERC20PausableUpgradeable, ERC20Upgradeable)
204:     

['221']

221:     function transfer(address recipient, uint256 amount)
222:         public
223:         override(ERC20Upgradeable, IERC20)
224:         returns (bool)
225:     

['266']

266:     function emergencyWithdraw(address safeAccount) external 

['289']

289:     function transferFrom(address sender, address recipient, uint256 amount)
290:         public
291:         override(ERC20Upgradeable, IERC20)
292:         returns (bool)
293:     

['56']

56:     function initialize(address registryContractAddress) public initializer 

['48']

48:     function tokenAmountToWadWithTokenAddress(uint256 tokenAmount, address token)
49:         internal
50:         view
51:         returns (uint256, uint8)
52:     

[NonCritical-7] Default int values are manually set

Resolution

In instances where a new variable is defined, there is no need to set it to it's default value.

Num of instances: 1

Findings

Click to show findings

['330']

330:         uint256 totalUsdcTaken = 0; // <= FOUND

[NonCritical-8] Revert statements within external and public functions can be used to perform DOS attacks

Resolution

In Solidity, 'revert' statements are used to undo changes and throw an exception when certain conditions are not met. However, in public and external functions, improper use of revert can be exploited for Denial of Service (DoS) attacks. An attacker can intentionally trigger these 'revert' conditions, causing legitimate transactions to consistently fail. For example, if a function relies on specific conditions from user input or contract state, an attacker could manipulate these to continually force reverts, blocking the function's execution. Therefore, it's crucial to design contract logic to handle exceptions properly and avoid scenarios where revert can be predictably triggered by malicious actors. This includes careful input validation and considering alternative design patterns that are less susceptible to such abuses.

Num of instances: 32

Findings

Click to show findings

['114']

114:     function setMaxDepegThreshold(uint256 maxAuthorizedDepegPrice) external virtual {
115:         if (maxAuthorizedDepegPrice > BASIS_POINT_BASE) revert DepegThresholdTooHigh(); // <= FOUND
116: 
117:         AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
118:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
119: 
120:         if ($.maxDepegThreshold == maxAuthorizedDepegPrice) revert SameValue(); // <= FOUND
121:         $.maxDepegThreshold = maxAuthorizedDepegPrice;
122:         emit SetMaxDepegThreshold(maxAuthorizedDepegPrice);
123:     }

['47']

47:     function initializeTokenOracle(
48:         address token,
49:         address dataSource,
50:         uint64 timeout,
51:         bool isStablecoin
52:     ) external {
53:         if (token == address(0)) revert NullAddress(); // <= FOUND
54:         if (dataSource == address(0)) revert NullAddress(); // <= FOUND
55:         
56:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout(); // <= FOUND
57: 
58:         
59:         (, int256 answer,, uint256 updatedAt,) = IAggregator(dataSource).latestRoundData();
60:         if (answer <= 0 || updatedAt == 0 || block.timestamp > updatedAt + timeout) {
61:             revert OracleNotWorkingNotCurrent(); // <= FOUND
62:         }
63: 
64:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
65:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
66: 
67:         $.tokenToOracleInfo[token].dataSource = dataSource;
68:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
69:         $.tokenToOracleInfo[token].timeout = timeout;
70:     }

['315']

315:     function activateCBR(uint256 coefficient) external {
316:         
317:         if (coefficient > SCALAR_ONE) {
318:             revert CBRIsTooHigh(); // <= FOUND
319:         } else if (coefficient == 0) {
320:             revert CBRIsNull(); // <= FOUND
321:         }
322:         _requireOnlyAdmin();
323:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
324:         $.isCBROn = true;
325:         $._swapPaused = true;
326:         $.cbrCoef = coefficient;
327:         emit CBRActivated($.cbrCoef);
328:         emit SwapPaused();
329:     }

['333']

333:     function deactivateCBR() external {
334:         _requireOnlyAdmin();
335:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
336:         if ($.isCBROn == false) revert SameValue(); // <= FOUND
337:         $.isCBROn = false;
338:         emit CBRDeactivated();
339:     }

['344']

344:     function setRedeemFee(uint256 _redeemFee) external {
345:         if (_redeemFee > MAX_REDEEM_FEE) revert RedeemFeeTooBig(); // <= FOUND
346:         _requireOnlyAdmin();
347:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
348:         if ($.redeemFee == _redeemFee) revert SameValue(); // <= FOUND
349:         $.redeemFee = _redeemFee;
350:         emit RedeemFeeUpdated(_redeemFee);
351:     }

['701']

701:     function redeem(address rwaToken, uint256 amount, uint256 minAmountOut)
702:         external
703:         nonReentrant
704:         whenRedeemNotPaused
705:         whenNotPaused
706:     {
707:         
708:         if (amount == 0) {
709:             revert AmountIsZero(); // <= FOUND
710:         }
711: 
712:         
713:         if (!_daoCollateralStorageV0().tokenMapping.isUsd0Collateral(rwaToken)) {
714:             revert InvalidToken(); // <= FOUND
715:         }
716:         uint256 stableFee = _transferFee(amount, rwaToken);
717:         uint256 returnedCollateral =
718:             _burnStableTokenAndTransferCollateral(rwaToken, amount, stableFee);
719:         
720:         if (returnedCollateral < minAmountOut) {
721:             revert AmountTooLow(); // <= FOUND
722:         }
723:         emit Redeem(msg.sender, rwaToken, amount, returnedCollateral, stableFee);
724:     }

['727']

727:     function redeemDao(address rwaToken, uint256 amount) external nonReentrant {
728:         
729:         if (amount == 0) {
730:             revert AmountIsZero(); // <= FOUND
731:         }
732: 
733:         _requireOnlyAdmin();
734:         
735:         if (!_daoCollateralStorageV0().tokenMapping.isUsd0Collateral(rwaToken)) {
736:             revert InvalidToken(); // <= FOUND
737:         }
738:         uint256 returnedCollateral = _burnStableTokenAndTransferCollateral(rwaToken, amount, 0);
739:         emit Redeem(msg.sender, rwaToken, amount, returnedCollateral, 0);
740:     }

['762']

762:     function swapRWAtoStbcIntent(
763:         uint256[] calldata orderIdsToTake,
764:         Approval calldata approval,
765:         Intent calldata intent,
766:         bool partialMatching
767:     ) external nonReentrant whenNotPaused whenSwapNotPaused {
768:         if (block.timestamp > intent.deadline) {
769:             revert ExpiredSignature(intent.deadline); // <= FOUND
770:         }
771:         if (approval.deadline != intent.deadline) {
772:             revert InvalidDeadline(approval.deadline, intent.deadline); // <= FOUND
773:         }
774: 
775:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
776:         $.registryAccess.onlyMatchingRole(INTENT_MATCHING_ROLE);
777: 
778:         uint256 nonce = _useNonce(intent.recipient);
779: 
780:         
781:         bytes32 structHash = keccak256(
782:             abi.encode(
783:                 INTENT_TYPE_HASH,
784:                 intent.recipient,
785:                 intent.rwaToken,
786:                 intent.amountInTokenDecimals,
787:                 nonce,
788:                 intent.deadline
789:             )
790:         );
791: 
792:         bytes32 hash = _hashTypedDataV4(structHash);
793: 
794:         if (!SignatureChecker.isValidSignatureNow(intent.recipient, hash, intent.signature)) {
795:             revert InvalidSigner(intent.recipient); // <= FOUND
796:         }
797:         (uint256 amountInTokenDecimals, uint256 amountInUSD) = _swapRWAtoStbc(
798:             intent.recipient,
799:             intent.rwaToken,
800:             intent.amountInTokenDecimals,
801:             partialMatching,
802:             orderIdsToTake,
803:             approval
804:         );
805:         emit IntentMatched(
806:             intent.recipient, nonce, intent.rwaToken, amountInTokenDecimals, amountInUSD
807:         );
808:     }

['39']

39:     function setRoleAdmin(bytes32 role, bytes32 adminRole) external {
40:         if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
41:             revert NotAuthorized(); // <= FOUND
42:         }
43:         _setRoleAdmin(role, adminRole);
44:     }

['83']

83:     function setContract(bytes32 name, address contractAddress) external {
84:         
85:         if (contractAddress == address(0)) {
86:             revert NullAddress(); // <= FOUND
87:         }
88:         
89:         if (name == bytes32(0)) {
90:             revert InvalidName(); // <= FOUND
91:         }
92: 
93:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
94:         
95:         if (!IRegistryAccess($._registryAccess).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
96:             revert NotAuthorized(); // <= FOUND
97:         }
98: 
99:         $._contracts[name] = contractAddress;
100:         emit SetContract(name, contractAddress);
101:     }

['106']

106:     function getContract(bytes32 name) external view returns (address) {
107:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
108:         address _contract = $._contracts[name];
109:         
110:         if (_contract == address(0)) {
111:             revert NullAddress(); // <= FOUND
112:         }
113: 
114:         return _contract;
115:     }

['212']

212:     function updateMinimumUSDCAmountProvided(uint256 minimumUSDCAmount) external {
213:         if (minimumUSDCAmount < ONE_USDC) {
214:             
215:             revert AmountTooLow(); // <= FOUND
216:         }
217:         _requireOnlyAdmin();
218:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
219:         $.minimumUSDCAmountProvided = minimumUSDCAmount;
220:     }

['281']

281:     function withdrawUSDC(uint256 orderToCancel) external nonReentrant whenNotPaused {
282:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
283:         UsdcOrder storage order = $.orders[orderToCancel];
284: 
285:         if (!order.active) {
286:             
287:             revert OrderNotActive(); // <= FOUND
288:         }
289:         if (order.requester != msg.sender) {
290:             
291:             revert NotRequester(); // <= FOUND
292:         }
293: 
294:         uint256 amountToWithdraw = order.tokenAmount;
295:         order.active = false; 
296:         order.tokenAmount = 0; 
297: 
298:         
299:         $.usdcToken.safeTransfer(msg.sender, amountToWithdraw);
300: 
301:         emit Withdraw(msg.sender, orderToCancel, amountToWithdraw);
302:     }

['384']

384:     function provideUsd0ReceiveUSDC(
385:         address recipient,
386:         uint256 amountUsdcToTakeInNativeDecimals,
387:         uint256[] memory orderIdsToTake,
388:         bool partialMatchingAllowed
389:     ) external nonReentrant whenNotPaused returns (uint256) {
390:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
391:         uint256 usdcWadPrice = _getUsdcWadPrice();
392:         uint256 requiredUsd0Amount =
393:             _getUsd0WadEquivalent(amountUsdcToTakeInNativeDecimals, usdcWadPrice);
394:         if ($.usd0.balanceOf(msg.sender) < requiredUsd0Amount) {
395:             revert InsufficientUSD0Balance(); // <= FOUND
396:         }
397:         (uint256 unmatchedUsdcAmount,) = _provideUsd0ReceiveUSDC(
398:             recipient,
399:             amountUsdcToTakeInNativeDecimals,
400:             orderIdsToTake,
401:             partialMatchingAllowed,
402:             usdcWadPrice
403:         );
404:         return unmatchedUsdcAmount;
405:     }

['408']

408:     function provideUsd0ReceiveUSDCWithPermit(
409:         address recipient,
410:         uint256 amountUsdcToTakeInNativeDecimals,
411:         uint256[] memory orderIdsToTake,
412:         bool partialMatchingAllowed,
413:         uint256 usd0ToPermit,
414:         uint256 deadline,
415:         uint8 v,
416:         bytes32 r,
417:         bytes32 s
418:     ) external nonReentrant whenNotPaused returns (uint256) {
419:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
420:         uint256 usdcWadPrice = _getUsdcWadPrice();
421:         uint256 requiredUsd0Amount =
422:             _getUsd0WadEquivalent(amountUsdcToTakeInNativeDecimals, usdcWadPrice);
423:         
424:         if ($.usd0.balanceOf(msg.sender) < requiredUsd0Amount || usd0ToPermit < requiredUsd0Amount)
425:         {
426:             revert InsufficientUSD0Balance(); // <= FOUND
427:         }
428:         try IERC20Permit(address($.usd0)).permit(
429:             msg.sender, address(this), usd0ToPermit, deadline, v, r, s
430:         ) {} catch {} 
431:         (uint256 unmatchedUsdcAmount,) = _provideUsd0ReceiveUSDC(
432:             recipient,
433:             amountUsdcToTakeInNativeDecimals,
434:             orderIdsToTake,
435:             partialMatchingAllowed,
436:             usdcWadPrice
437:         );
438:         return unmatchedUsdcAmount;
439:     }

['85']

85:     function addUsd0Rwa(address rwa) external returns (bool) {
86:         if (rwa == address(0)) {
87:             revert NullAddress(); // <= FOUND
88:         }
89:         
90:         
91:         
92:         if (IERC20Metadata(rwa).decimals() == 0) {
93:             revert Invalid(); // <= FOUND
94:         }
95: 
96:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
97:         $._registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
98: 
99:         
100:         if ($.isUsd0Collateral[rwa]) revert SameValue(); // <= FOUND
101:         $.isUsd0Collateral[rwa] = true;
102:         
103:         ++$._usd0ToRwaLastId;
104:         if ($._usd0ToRwaLastId > MAX_RWA_COUNT) {
105:             revert TooManyRWA(); // <= FOUND
106:         }
107:         $.USD0Rwas[$._usd0ToRwaLastId] = rwa;
108:         emit AddUsd0Rwa(rwa, $._usd0ToRwaLastId);
109:         return true;
110:     }

['117']

117:     function getUsd0RwaById(uint256 rwaId) external view returns (address) {
118:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
119:         address rwa = $.USD0Rwas[rwaId];
120:         if (rwa == address(0)) {
121:             revert InvalidToken(); // <= FOUND
122:         }
123:         return rwa;
124:     }

['181']

181:     function blacklist(address account) external {
182:         if (account == address(0)) {
183:             revert NullAddress(); // <= FOUND
184:         }
185:         Usd0StorageV0 storage $ = _usd0StorageV0();
186:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
187:         if ($.isBlacklisted[account]) {
188:             revert SameValue(); // <= FOUND
189:         }
190:         $.isBlacklisted[account] = true;
191: 
192:         emit Blacklist(account);
193:     }

['198']

198:     function unBlacklist(address account) external {
199:         Usd0StorageV0 storage $ = _usd0StorageV0();
200:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
201:         if (!$.isBlacklisted[account]) {
202:             revert SameValue(); // <= FOUND
203:         }
204:         $.isBlacklisted[account] = false;
205: 
206:         emit UnBlacklist(account);
207:     }

['230']

230:     function unwrap() external nonReentrant whenNotPaused {
231:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
232: 
233:         
234:         if (block.timestamp < $.bondStart + BOND_DURATION_FOUR_YEAR) {
235:             revert BondNotFinished(); // <= FOUND
236:         }
237:         uint256 usd0PPBalance = balanceOf(msg.sender);
238: 
239:         _burn(msg.sender, usd0PPBalance);
240: 
241:         $.usd0.safeTransfer(msg.sender, usd0PPBalance);
242: 
243:         emit BondUnwrapped(msg.sender, usd0PPBalance);
244:     }

['266']

266:     function emergencyWithdraw(address safeAccount) external {
267:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
268: 
269:         if (!$.registryAccess.hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
270:             revert NotAuthorized(); // <= FOUND
271:         }
272:         IERC20 usd0 = $.usd0;
273: 
274:         uint256 balance = usd0.balanceOf(address(this));
275:         
276:         usd0.safeTransfer(safeAccount, balance);
277: 
278:         
279:         _pause();
280: 
281:         emit EmergencyWithdraw(safeAccount, balance);
282:     }

['69']

69:     function initializeTokenOracle(address token, uint64 timeout, bool isStablecoin) external {
70:         if (token == address(0)) revert NullAddress(); // <= FOUND
71:         
72:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout(); // <= FOUND
73: 
74:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
75:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
76: 
77:         UsualOracleStorageV0 storage u = _usualOracleStorageV0();
78: 
79:         
80:         (, int256 answer, uint256 timestamp,) = u.dataPublisher.latestRoundData(token);
81:         if (answer <= 0 || block.timestamp - timestamp > timeout) {
82:             revert OracleNotWorkingNotCurrent(); // <= FOUND
83:         }
84: 
85:         $.tokenToOracleInfo[token].dataSource = address(u.dataPublisher);
86:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
87:     }

['260']

260:     function initialize(address _registryContract, uint256 _redeemFee) public initializer {
261:         
262:         if (_redeemFee > MAX_REDEEM_FEE) {
263:             revert RedeemFeeTooBig(); // <= FOUND
264:         }
265: 
266:         if (_registryContract == address(0)) {
267:             revert NullContract(); // <= FOUND
268:         }
269: 
270:         __EIP712_init_unchained("DaoCollateral", "1");
271:         __Nonces_init_unchained();
272:         __Pausable_init_unchained();
273:         __ReentrancyGuard_init_unchained();
274: 
275:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
276:         $.redeemFee = _redeemFee;
277: 
278:         IRegistryContract registryContract = IRegistryContract(_registryContract);
279:         $.registryAccess = IRegistryAccess(registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
280: 
281:         $.treasury = address(registryContract.getContract(CONTRACT_TREASURY));
282:         $.tokenMapping = ITokenMapping(registryContract.getContract(CONTRACT_TOKEN_MAPPING));
283:         $.usd0 = IUsd0(registryContract.getContract(CONTRACT_USD0));
284: 
285:         $.oracle = IOracle(registryContract.getContract(CONTRACT_ORACLE));
286: 
287:         $.swapperEngine = ISwapperEngine(registryContract.getContract(CONTRACT_SWAPPER_ENGINE));
288:     }

['293']

293:     function initializeV1(address _registryContract) public reinitializer(2) {
294:         if (_registryContract == address(0)) {
295:             revert NullContract(); // <= FOUND
296:         }
297: 
298:         __EIP712_init_unchained("DaoCollateral", "1");
299:         __Nonces_init_unchained();
300: 
301:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
302:         $.registryContract = IRegistryContract(_registryContract);
303:         $.swapperEngine = ISwapperEngine(
304:             IRegistryContract(_registryContract).getContract(CONTRACT_SWAPPER_ENGINE)
305:         );
306:     }

['668']

668:     function swap(address rwaToken, uint256 amount, uint256 minAmountOut)
669:         public
670:         nonReentrant
671:         whenSwapNotPaused
672:         whenNotPaused
673:     {
674:         uint256 wadQuoteInUSD = _swapCheckAndGetUSDQuote(rwaToken, amount);
675:         
676:         if (wadQuoteInUSD < minAmountOut) {
677:             revert AmountTooLow(); // <= FOUND
678:         }
679:         _transferRWATokenAndMintStable(rwaToken, amount, wadQuoteInUSD);
680:         
681:         emit Swap(msg.sender, rwaToken, amount, wadQuoteInUSD);
682:     }

['27']

27:     function initialize(address deployer) public initializer {
28:         
29:         if (deployer == address(0)) {
30:             revert NullAddress(); // <= FOUND
31:         }
32:         __AccessControl_init_unchained();
33:         __AccessControlDefaultAdminRules_init_unchained(
34:             3 days,
35:             deployer 
36:         );
37:     }

['67']

67:     function initialize(address registryAccess_) public initializer {
68:         if (registryAccess_ == address(0)) {
69:             revert NullAddress(); // <= FOUND
70:         }
71: 
72:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
73:         $._registryAccess = registryAccess_;
74:     }

['124']

124:     function initialize(address registryContract) public initializer {
125:         if (registryContract == address(0)) {
126:             revert NullContract(); // <= FOUND
127:         }
128:         __Pausable_init_unchained();
129:         __ReentrancyGuard_init_unchained();
130:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
131:         $.registryContract = IRegistryContract(registryContract);
132:         $.registryAccess = IRegistryAccess($.registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
133:         $.usdcToken = IERC20($.registryContract.getContract(CONTRACT_USDC));
134:         $.usd0 = IERC20($.registryContract.getContract(CONTRACT_USD0));
135:         $.nextOrderId = 1;
136:         $.oracle = IOracle($.registryContract.getContract(CONTRACT_ORACLE));
137:         $.minimumUSDCAmountProvided = MINIMUM_USDC_PROVIDED;
138:     }

['74']

74:     function initialize(address registryAccess, address registryContract) public initializer {
75:         if (registryAccess == address(0) || registryContract == address(0)) {
76:             revert NullAddress(); // <= FOUND
77:         }
78: 
79:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
80:         $._registryAccess = IRegistryAccess(registryAccess);
81:         $._registryContract = IRegistryContract(registryContract);
82:     }

['67']

67:     function initialize(address registryContract_, string memory name_, string memory symbol_)
68:         public
69:         initializer
70:     {
71:         
72:         __ERC20_init_unchained(name_, symbol_);
73:         
74:         __Pausable_init_unchained();
75:         
76:         __ERC20Permit_init_unchained(name_);
77:         
78:         __EIP712_init_unchained(name_, "1");
79:         
80:         if (registryContract_ == address(0)) {
81:             revert NullContract(); // <= FOUND
82:         }
83:         _usd0StorageV0().registryAccess = IRegistryAccess(
84:             IRegistryContract(registryContract_).getContract(CONTRACT_REGISTRY_ACCESS)
85:         );
86:     }

['135']

135:     function mint(address to, uint256 amount) public {
136:         if (amount == 0) {
137:             revert AmountIsZero(); // <= FOUND
138:         }
139: 
140:         Usd0StorageV0 storage $ = _usd0StorageV0();
141:         $.registryAccess.onlyMatchingRole(USD0_MINT);
142:         _mint(to, amount);
143:     }

['169']

169:     function mint(uint256 amountUsd0) public nonReentrant whenNotPaused {
170:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
171: 
172:         
173:         if (block.timestamp < $.bondStart) {
174:             revert BondNotStarted(); // <= FOUND
175:         }
176:         
177:         if (block.timestamp >= $.bondStart + BOND_DURATION_FOUR_YEAR) {
178:             revert BondFinished(); // <= FOUND
179:         }
180: 
181:         
182:         $.usd0.safeTransferFrom(msg.sender, address(this), amountUsd0);
183: 
184:         
185:         _mint(msg.sender, amountUsd0);
186:     }

[NonCritical-9] Contract lines should not be longer than 120 characters for readability

Resolution

Consider spreading these lines over multiple lines to aid in readability and the support of VIM users everywhere.

Num of instances: 19

Findings

Click to show findings

['7']

7: /// @dev CurveGauge interface documentation https://docs.curve.fi/curve_dao/liquidity-gauge-and-minting-crv/gauges/PermissionlessRewards/ // <= FOUND

['38']

38:     /// @param amountUsdcToTakeInNativeDecimals The amount of USDC tokens to take, in the token's native decimal representation. // <= FOUND

['56']

56:     /// @param usd0ToPermit The amount of USD0 tokens to permit, must be greater than the equivalent amount of USDC tokens to take. // <= FOUND

['73']

73:     /// @notice Allows users to specify an amount of USD0 tokens to swap and receive USDC tokens by matching against existing orders. // <= FOUND

['42']

42: /// @notice  Manages bond-like financial instruments for the UsualDAO ecosystem, providing functionality for minting, transferring, and unwrapping bonds. // <= FOUND

['43']

43: /// @dev     Inherits from ERC20, ERC20PermitUpgradeable, and ReentrancyGuardUpgradeable to provide a range of functionalities along with protections against reentrancy attacks. // <= FOUND

['14']

14:     /// @dev    This function fetches the latest available price from a Chainlink-compatible feed for the token and computes the quote based on the given amount. // <= FOUND

['68']

68: /// @notice  Manages the swapping of collateral tokens for stablecoins, with functionalities for swap (direct mint) and redeeming tokens // <= FOUND

['407']

407:     /// @notice  _swapCheckAndGetUSDQuote method will check if the token is a USD0-supported RWA token and if the amount is not 0 // <= FOUND

['546']

546:     /// @notice  _burnStableTokenAndTransferCollateral method will burn the stable token and transfer the collateral token // <= FOUND

['55']

55: ///      + Low Slippage: The price of the swap is determined by the usdc oracle price at that block height irrespective of liquidity depth. // <= FOUND

['58']

58: ///      - Match to Market: The price of the swaps is determined by the individual orders when they are executed not when they are placed (offset by low price volatility of stables) // <= FOUND

['59']

59: /// @custom:mechanism Effectively this facilitates RWA --> USD0 --> USDC --> $$$ --> RWA ... limited only by USDC orderbook depth // <= FOUND

['175']

175:     ///@dev This function converts the USDC token amount from its native decimal representation to WAD format (18 decimals), // <= FOUND

['177']

177:     ///@param usdcTokenAmountInNativeDecimals The amount of USDC tokens in their native decimal representation (6 decimals). // <= FOUND

['192']

192:     /// @dev This function calculates the expected USDC amount to receive based on the provided usd0WadAmount and USDC price in WAD format. // <= FOUND

['195']

195:     /// @return usdcTokenAmountInNativeDecimals The equivalent amount of USDC tokens in their native decimal representation. // <= FOUND

['341']

341:                 // if the usdcOrder tokenAmount > remainingAmountToTake only take the remaining else take the whole order // <= FOUND

['17']

17: /// @dev     This contract provides functionalities to link Real World Assets (RWA) tokens with Stable Coin (Usd0) tokens and manage token pairs. // <= FOUND

[NonCritical-10] Not all event definitions are utilizing indexed variables.

Resolution

Try to index as much as three variables in event declarations as this is more gas efficient when done on value type variables (uint, address etc) however not for bytes and string variables

Num of instances: 11

Findings

Click to show findings

['120']

120: event Swap( // <= FOUND
121:         address indexed owner, address indexed tokenSwapped, uint256 amount, uint256 amountInUSD
122:     );

['125']

125: event Redeem( // <= FOUND
126:         address indexed redeemer,
127:         address indexed rwaToken,
128:         uint256 amountRedeemed,
129:         uint256 returnedRwaAmount,
130:         uint256 stableFeeAmount
131:     );

['158']

158: event CBRActivated(uint256 cbrCoef); // <= FOUND

['165']

165: event RedeemFeeUpdated(uint256 redeemFee); // <= FOUND

['88']

88: event Deposit(address indexed requester, uint256 indexed orderId, uint256 amount); // <= FOUND

['89']

89: event Withdraw(address indexed requester, uint256 indexed orderId, uint256 amount); // <= FOUND

['30']

30: event Blacklist(address account); // <= FOUND

['31']

31: event UnBlacklist(address account); // <= FOUND

['59']

59: event BondUnwrapped(address indexed user, uint256 amount); // <= FOUND

['64']

64: event EmergencyWithdraw(address indexed account, uint256 balance); // <= FOUND

['44']

44: event SetMaxDepegThreshold(uint256 newMaxDepegThreshold); // <= FOUND

[NonCritical-11] Contracts should have all public/external functions exposed by interfaces

Resolution

Contracts should expose all public and external functions through interfaces. This practice ensures a clear and consistent definition of how the contract can be interacted with, promoting better transparency and integration.

Num of instances: 70

Findings

Click to show findings

['104']

104:     function getMaxDepegThreshold() external view returns (uint256) 

['47']

47:     function initializeTokenOracle(
48:         address token,
49:         address dataSource,
50:         uint64 timeout,
51:         bool isStablecoin
52:     ) external 

['315']

315:     function activateCBR(uint256 coefficient) external 

['333']

333:     function deactivateCBR() external 

['344']

344:     function setRedeemFee(uint256 _redeemFee) external 

['355']

355:     function pauseRedeem() external whenRedeemNotPaused 

['364']

364:     function unpauseRedeem() external whenRedeemPaused 

['373']

373:     function pauseSwap() external whenSwapNotPaused 

['382']

382:     function unpauseSwap() external whenSwapPaused 

['391']

391:     function pause() external 

['162']

162:     function unpause() external 

['685']

685:     function swapWithPermit(
686:         address rwaToken,
687:         uint256 amount,
688:         uint256 minAmountOut,
689:         uint256 deadline,
690:         uint8 v,
691:         bytes32 r,
692:         bytes32 s
693:     ) external 

['701']

701:     function redeem(address rwaToken, uint256 amount, uint256 minAmountOut)
702:         external
703:         nonReentrant
704:         whenRedeemNotPaused
705:         whenNotPaused
706:     

['727']

727:     function redeemDao(address rwaToken, uint256 amount) external nonReentrant 

['743']

743:     function swapRWAtoStbc(
744:         address rwaToken,
745:         uint256 amountInTokenDecimals,
746:         bool partialMatching,
747:         uint256[] calldata orderIdsToTake,
748:         Approval calldata approval
749:     ) external nonReentrant whenNotPaused whenSwapNotPaused 

['756']

756:     function invalidateNonce() external 

['762']

762:     function swapRWAtoStbcIntent(
763:         uint256[] calldata orderIdsToTake,
764:         Approval calldata approval,
765:         Intent calldata intent,
766:         bool partialMatching
767:     ) external nonReentrant whenNotPaused whenSwapNotPaused 

['817']

817:     function isCBROn() external view returns (bool) 

['39']

39:     function setRoleAdmin(bytes32 role, bytes32 adminRole) external 

['83']

83:     function setContract(bytes32 name, address contractAddress) external 

['106']

106:     function getContract(bytes32 name) external view returns (address) 

['212']

212:     function updateMinimumUSDCAmountProvided(uint256 minimumUSDCAmount) external 

['391']

391:     function pause() external 

['162']

162:     function unpause() external 

['260']

260:     function depositUSDC(uint256 amountToDeposit) external nonReentrant whenNotPaused 

['266']

266:     function depositUSDCWithPermit(
267:         uint256 amountToDeposit,
268:         uint256 deadline,
269:         uint8 v,
270:         bytes32 r,
271:         bytes32 s
272:     ) external nonReentrant whenNotPaused 

['281']

281:     function withdrawUSDC(uint256 orderToCancel) external nonReentrant whenNotPaused 

['384']

384:     function provideUsd0ReceiveUSDC(
385:         address recipient,
386:         uint256 amountUsdcToTakeInNativeDecimals,
387:         uint256[] memory orderIdsToTake,
388:         bool partialMatchingAllowed
389:     ) external nonReentrant whenNotPaused returns (uint256) 

['408']

408:     function provideUsd0ReceiveUSDCWithPermit(
409:         address recipient,
410:         uint256 amountUsdcToTakeInNativeDecimals,
411:         uint256[] memory orderIdsToTake,
412:         bool partialMatchingAllowed,
413:         uint256 usd0ToPermit,
414:         uint256 deadline,
415:         uint8 v,
416:         bytes32 r,
417:         bytes32 s
418:     ) external nonReentrant whenNotPaused returns (uint256) 

['442']

442:     function swapUsd0(
443:         address recipient,
444:         uint256 amountUsd0ToProvideInWad,
445:         uint256[] memory orderIdsToTake,
446:         bool partialMatchingAllowed
447:     ) external nonReentrant whenNotPaused returns (uint256) 

['85']

85:     function addUsd0Rwa(address rwa) external returns (bool) 

['117']

117:     function getUsd0RwaById(uint256 rwaId) external view returns (address) 

['127']

127:     function getAllUsd0Rwa() external view returns (address[] memory) 

['142']

142:     function getLastUsd0RwaId() external view returns (uint256) 

['148']

148:     function isUsd0Collateral(address rwa) external view returns (bool) 

['391']

391:     function pause() external 

['162']

162:     function unpause() external 

['181']

181:     function blacklist(address account) external 

['198']

198:     function unBlacklist(address account) external 

['210']

210:     function isBlacklisted(address account) external view returns (bool) 

['162']

162:     function unpause() external 

['34']

34:     function mintWithPermit(uint256 amountUsd0, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
35:         external
36:     

['230']

230:     function unwrap() external nonReentrant whenNotPaused 

['252']

252:     function getStartTime() external view returns (uint256) 

['258']

258:     function getEndTime() external view returns (uint256) 

['266']

266:     function emergencyWithdraw(address safeAccount) external 

['69']

69:     function initializeTokenOracle(address token, uint64 timeout, bool isStablecoin) external 

['124']

124:     function initialize(address registryContract) public initializer 

['260']

260:     function initialize(address _registryContract, uint256 _redeemFee) public initializer 

['293']

293:     function initializeV1(address _registryContract) public reinitializer(2) 

['668']

668:     function swap(address rwaToken, uint256 amount, uint256 minAmountOut)
669:         public
670:         nonReentrant
671:         whenSwapNotPaused
672:         whenNotPaused
673:     

['823']

823:     function cbrCoef() public view returns (uint256) 

['829']

829:     function redeemFee() public view returns (uint256) 

['835']

835:     function isRedeemPaused() public view returns (bool) 

['841']

841:     function isSwapPaused() public view returns (bool) 

['27']

27:     function initialize(address deployer) public initializer 

['67']

67:     function initialize(address registryAccess_) public initializer 

['124']

124:     function initialize(address registryContract) public initializer 

['149']

149:     function minimumUSDCAmountProvided() public view returns (uint256 minimumUSDCAmount) 

['167']

167:     function getOrder(uint256 orderId) public view returns (bool active, uint256 tokenAmount) 

['74']

74:     function initialize(address registryAccess, address registryContract) public initializer 

['67']

67:     function initialize(address registryContract_, string memory name_, string memory symbol_)
68:         public
69:         initializer
70:     

['135']

135:     function mint(address to, uint256 amount) public 

['146']

146:     function burnFrom(address account, uint256 amount) public 

['154']

154:     function burn(uint256 amount) public 

['108']

108:     function initialize(
109:         address registryContract,
110:         string memory name_,
111:         string memory symbol_,
112:         uint256 startTime
113:     ) public initializer 

['154']

154:     function pause() public 

['169']

169:     function mint(uint256 amountUsd0) public nonReentrant whenNotPaused 

['247']

247:     function totalBondTimes() public pure returns (uint256) 

['56']

56:     function initialize(address registryContractAddress) public initializer 

[NonCritical-12] Functions within contracts are not ordered according to the solidity style guide

Resolution

The following order should be used within contracts

constructor

receive function (if exists)

fallback function (if exists)

external

public

internal

private

Rearrange the contract functions and contructors to fit this ordering

Num of instances: 10

Findings

Click to show findings

['23']

23: contract ClassicalOracle is IOracle, AbstractOracle  // <= FOUND

[]

71: contract DaoCollateral is
72:     IDaoCollateral,
73:     Initializable,
74:     ReentrancyGuardUpgradeable,
75:     PausableUpgradeable,
76:     NoncesUpgradeable,
77:     EIP712Upgradeable
78: 

['14']

14: contract RegistryAccess is AccessControlDefaultAdminRulesUpgradeable, IRegistryAccess  // <= FOUND

['16']

16: contract RegistryContract is IRegistryContract, Initializable  // <= FOUND

[]

61: contract SwapperEngine is
62:     Initializable,
63:     ReentrancyGuardUpgradeable,
64:     PausableUpgradeable,
65:     ISwapperEngine
66: 

['20']

20: contract TokenMapping is ITokenMapping, Initializable  // <= FOUND

['26']

26: contract Usd0 is ERC20PausableUpgradeable, ERC20PermitUpgradeable, IUsd0  // <= FOUND

[]

47: contract Usd0PP is
48:     IUsd0PP,
49:     ERC20PausableUpgradeable,
50:     ERC20PermitUpgradeable,
51:     ReentrancyGuardUpgradeable
52: 

['22']

22: contract UsualOracle is AbstractOracle  // <= FOUND

['25']

25: abstract contract AbstractOracle is IOracle, Initializable  // <= FOUND

[NonCritical-13] Emits without msg.sender parameter

Resolution

In Solidity, when msg.sender plays a crucial role in a function's logic, it's important for transparency and auditability that any events emitted by this function include msg.sender as a parameter. This practice enhances the traceability and accountability of transactions, allowing users and external observers to easily track who initiated a particular action. Including msg.sender in event logs helps in creating a clear and verifiable record of interactions with the contract, thereby increasing user trust and facilitating easier debugging and analysis of contract behavior. It's a key aspect of writing clear, transparent, and user-friendly smart contracts.

Num of instances: 2

Findings

Click to show findings

['83']

83:     function setContract(bytes32 name, address contractAddress) external {
84:         
85:         if (contractAddress == address(0)) {
86:             revert NullAddress();
87:         }
88:         
89:         if (name == bytes32(0)) {
90:             revert InvalidName();
91:         }
92: 
93:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
94:         
95:         if (!IRegistryAccess($._registryAccess).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { // <= FOUND
96:             revert NotAuthorized();
97:         }
98: 
99:         $._contracts[name] = contractAddress;
100:         emit SetContract(name, contractAddress); // <= FOUND
101:     }

['266']

266:     function emergencyWithdraw(address safeAccount) external {
267:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
268: 
269:         if (!$.registryAccess.hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { // <= FOUND
270:             revert NotAuthorized();
271:         }
272:         IERC20 usd0 = $.usd0;
273: 
274:         uint256 balance = usd0.balanceOf(address(this));
275:         
276:         usd0.safeTransfer(safeAccount, balance);
277: 
278:         
279:         _pause();
280: 
281:         emit EmergencyWithdraw(safeAccount, balance); // <= FOUND
282:     }

[NonCritical-14] Functions with array parameters should have length checks in place

Resolution

Functions in Solidity that accept array parameters should incorporate length checks as a security measure. This is to prevent potential overflow errors, unwanted gas consumption, and manipulation attempts. Without length checks, an attacker could pass excessively large arrays as input, causing excessive computation and potentially causing the function to exceed the block gas limit, leading to a denial-of-service. Additionally, unexpected array sizes could lead to logic errors within the function. As a resolution, always validate array length at the start of functions handling array inputs, ensuring it aligns with the expectations of the function logic. This makes the code more robust and predictable.

Num of instances: 5

Findings

Click to show findings

['743']

743:     function swapRWAtoStbc(
744:         address rwaToken,
745:         uint256 amountInTokenDecimals,
746:         bool partialMatching,
747:         uint256[] calldata orderIdsToTake, // <= FOUND
748:         Approval calldata approval
749:     ) external nonReentrant whenNotPaused whenSwapNotPaused {
750:         _swapRWAtoStbc(
751:             msg.sender, rwaToken, amountInTokenDecimals, partialMatching, orderIdsToTake, approval
752:         );
753:     }

['762']

762:     function swapRWAtoStbcIntent(
763:         uint256[] calldata orderIdsToTake, // <= FOUND
764:         Approval calldata approval,
765:         Intent calldata intent,
766:         bool partialMatching
767:     ) external nonReentrant whenNotPaused whenSwapNotPaused {
768:         if (block.timestamp > intent.deadline) {
769:             revert ExpiredSignature(intent.deadline);
770:         }
771:         if (approval.deadline != intent.deadline) {
772:             revert InvalidDeadline(approval.deadline, intent.deadline);
773:         }
774: 
775:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
776:         $.registryAccess.onlyMatchingRole(INTENT_MATCHING_ROLE);
777: 
778:         uint256 nonce = _useNonce(intent.recipient);
779: 
780:         
781:         bytes32 structHash = keccak256(
782:             abi.encode(
783:                 INTENT_TYPE_HASH,
784:                 intent.recipient,
785:                 intent.rwaToken,
786:                 intent.amountInTokenDecimals,
787:                 nonce,
788:                 intent.deadline
789:             )
790:         );
791: 
792:         bytes32 hash = _hashTypedDataV4(structHash);
793: 
794:         if (!SignatureChecker.isValidSignatureNow(intent.recipient, hash, intent.signature)) {
795:             revert InvalidSigner(intent.recipient);
796:         }
797:         (uint256 amountInTokenDecimals, uint256 amountInUSD) = _swapRWAtoStbc(
798:             intent.recipient,
799:             intent.rwaToken,
800:             intent.amountInTokenDecimals,
801:             partialMatching,
802:             orderIdsToTake,
803:             approval
804:         );
805:         emit IntentMatched(
806:             intent.recipient, nonce, intent.rwaToken, amountInTokenDecimals, amountInUSD
807:         );
808:     }

['384']

384:     function provideUsd0ReceiveUSDC(
385:         address recipient,
386:         uint256 amountUsdcToTakeInNativeDecimals,
387:         uint256[] memory orderIdsToTake, // <= FOUND
388:         bool partialMatchingAllowed
389:     ) external nonReentrant whenNotPaused returns (uint256) {
390:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
391:         uint256 usdcWadPrice = _getUsdcWadPrice();
392:         uint256 requiredUsd0Amount =
393:             _getUsd0WadEquivalent(amountUsdcToTakeInNativeDecimals, usdcWadPrice);
394:         if ($.usd0.balanceOf(msg.sender) < requiredUsd0Amount) {
395:             revert InsufficientUSD0Balance();
396:         }
397:         (uint256 unmatchedUsdcAmount,) = _provideUsd0ReceiveUSDC(
398:             recipient,
399:             amountUsdcToTakeInNativeDecimals,
400:             orderIdsToTake,
401:             partialMatchingAllowed,
402:             usdcWadPrice
403:         );
404:         return unmatchedUsdcAmount;
405:     }

['408']

408:     function provideUsd0ReceiveUSDCWithPermit(
409:         address recipient,
410:         uint256 amountUsdcToTakeInNativeDecimals,
411:         uint256[] memory orderIdsToTake, // <= FOUND
412:         bool partialMatchingAllowed,
413:         uint256 usd0ToPermit,
414:         uint256 deadline,
415:         uint8 v,
416:         bytes32 r,
417:         bytes32 s
418:     ) external nonReentrant whenNotPaused returns (uint256) {
419:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
420:         uint256 usdcWadPrice = _getUsdcWadPrice();
421:         uint256 requiredUsd0Amount =
422:             _getUsd0WadEquivalent(amountUsdcToTakeInNativeDecimals, usdcWadPrice);
423:         
424:         if ($.usd0.balanceOf(msg.sender) < requiredUsd0Amount || usd0ToPermit < requiredUsd0Amount)
425:         {
426:             revert InsufficientUSD0Balance();
427:         }
428:         try IERC20Permit(address($.usd0)).permit(
429:             msg.sender, address(this), usd0ToPermit, deadline, v, r, s
430:         ) {} catch {} 
431:         (uint256 unmatchedUsdcAmount,) = _provideUsd0ReceiveUSDC(
432:             recipient,
433:             amountUsdcToTakeInNativeDecimals,
434:             orderIdsToTake,
435:             partialMatchingAllowed,
436:             usdcWadPrice
437:         );
438:         return unmatchedUsdcAmount;
439:     }

['442']

442:     function swapUsd0(
443:         address recipient,
444:         uint256 amountUsd0ToProvideInWad,
445:         uint256[] memory orderIdsToTake, // <= FOUND
446:         bool partialMatchingAllowed
447:     ) external nonReentrant whenNotPaused returns (uint256) {
448:         uint256 usdcWadPrice = _getUsdcWadPrice();
449: 
450:         (, uint256 totalUsd0Provided) = _provideUsd0ReceiveUSDC(
451:             recipient,
452:             _getUsdcAmountFromUsd0WadEquivalent(amountUsd0ToProvideInWad, usdcWadPrice),
453:             orderIdsToTake,
454:             partialMatchingAllowed,
455:             usdcWadPrice
456:         );
457: 
458:         return amountUsd0ToProvideInWad - totalUsd0Provided;
459:     }

[NonCritical-15] All interfaces used within a project should be imported

Num of instances: 3

Findings

Click to show findings

['8']

8: interface IGauge is IERC20Metadata  // <= FOUND

['7']

7: interface IRwaMock is IERC20Metadata  // <= FOUND

['5']

5: interface IDistributor  // <= FOUND

[NonCritical-16] A function which defines named returns in it's declaration doesn't need to use return

Resolution

Refacter the code to assign to the named return variables rather than using a return statement

Num of instances: 2

Findings

Click to show findings

['587']

587:     function _swapRWAtoStbc(
588:         address caller,
589:         address rwaToken,
590:         uint256 amountInTokenDecimals,
591:         bool partialMatching,
592:         uint256[] calldata orderIdsToTake,
593:         Approval calldata approval
594:     ) internal returns (uint256 matchedAmountInTokenDecimals, uint256 matchedAmountInUSD) {
595:         if (amountInTokenDecimals == 0) {
596:             revert AmountIsZero();
597:         }
598:         if (amountInTokenDecimals > type(uint128).max) {
599:             revert AmountTooBig();
600:         }
601:         if (orderIdsToTake.length == 0) {
602:             revert NoOrdersIdsProvided();
603:         }
604:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
605:         if (!$.tokenMapping.isUsd0Collateral(rwaToken)) {
606:             revert InvalidToken();
607:         }
608: 
609:         
610:         if (approval.deadline != 0 && approval.v != 0 && approval.r != 0 && approval.s != 0) {
611:             
612:             try IERC20Permit(rwaToken).permit(
613:                 caller,
614:                 address(this),
615:                 amountInTokenDecimals,
616:                 approval.deadline,
617:                 approval.v,
618:                 approval.r,
619:                 approval.s
620:             ) {} catch {} 
621:         }
622: 
623:         
624:         IERC20Metadata(rwaToken).safeTransferFrom(caller, address(this), amountInTokenDecimals);
625:         
626:         uint256 wadRwaQuoteInUSD = _getQuoteInUsd(amountInTokenDecimals, rwaToken);
627:         
628:         $.usd0.mint(address(this), wadRwaQuoteInUSD);
629:         if (!IERC20($.usd0).approve(address($.swapperEngine), wadRwaQuoteInUSD)) {
630:             revert ApprovalFailed();
631:         }
632:         
633:         uint256 wadRwaNotTakenInUSD =
634:             $.swapperEngine.swapUsd0(caller, wadRwaQuoteInUSD, orderIdsToTake, partialMatching);
635: 
636:         
637:         if (wadRwaNotTakenInUSD > 0) {
638:             if (!IERC20($.usd0).approve(address($.swapperEngine), 0)) {
639:                 revert ApprovalFailed();
640:             }
641:             $.usd0.burnFrom(address(this), wadRwaNotTakenInUSD);
642: 
643:             
644:             uint256 rwaTokensToReturn = _getQuoteInToken(wadRwaNotTakenInUSD, rwaToken);
645: 
646:             
647:             IERC20Metadata(rwaToken).safeTransfer(caller, rwaTokensToReturn);
648: 
649:             matchedAmountInTokenDecimals = amountInTokenDecimals - rwaTokensToReturn;
650:         } else {
651:             matchedAmountInTokenDecimals = amountInTokenDecimals;
652:         }
653: 
654:         
655:         IERC20Metadata(rwaToken).safeTransfer($.treasury, matchedAmountInTokenDecimals);
656: 
657:         matchedAmountInUSD = wadRwaQuoteInUSD - wadRwaNotTakenInUSD;
658:         emit Swap(caller, rwaToken, matchedAmountInTokenDecimals, matchedAmountInUSD);
659: 
660:         return (matchedAmountInTokenDecimals, matchedAmountInUSD); // <= FOUND
661:     }

['313']

313:     function _provideUsd0ReceiveUSDC( 
314:         address recipient,
315:         uint256 amountUsdcToTakeInNativeDecimals,
316:         uint256[] memory orderIdsToTake,
317:         bool partialMatchingAllowed,
318:         uint256 usdcWadPrice
319:     ) internal returns (uint256 unmatchedUsdcAmount, uint256 totalUsd0Provided) {
320:         if (amountUsdcToTakeInNativeDecimals == 0) {
321:             
322:             revert AmountIsZero();
323:         }
324:         if (orderIdsToTake.length == 0) {
325:             revert NoOrdersIdsProvided();
326:         }
327: 
328:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
329: 
330:         uint256 totalUsdcTaken = 0;
331: 
332:         for (
333:             uint256 i;
334:             i < orderIdsToTake.length && totalUsdcTaken < amountUsdcToTakeInNativeDecimals;
335:         ) {
336:             uint256 orderId = orderIdsToTake[i];
337:             UsdcOrder storage order = $.orders[orderId];
338: 
339:             if (order.active) {
340:                 uint256 remainingAmountToTake = amountUsdcToTakeInNativeDecimals - totalUsdcTaken;
341:                 
342:                 uint256 amountOfUsdcFromOrder = order.tokenAmount > remainingAmountToTake
343:                     ? remainingAmountToTake
344:                     : order.tokenAmount;
345: 
346:                 
347:                 
348:                 
349: 
350:                 order.tokenAmount -= amountOfUsdcFromOrder;
351:                 totalUsdcTaken += amountOfUsdcFromOrder;
352: 
353:                 if (order.tokenAmount == 0) {
354:                     order.active = false;
355:                 }
356: 
357:                 uint256 usd0Amount = _getUsd0WadEquivalent(amountOfUsdcFromOrder, usdcWadPrice);
358:                 totalUsd0Provided += usd0Amount;
359:                 
360:                 $.usd0.safeTransferFrom(msg.sender, order.requester, usd0Amount);
361: 
362:                 emit OrderMatched(order.requester, msg.sender, orderId, amountOfUsdcFromOrder);
363:             }
364: 
365:             unchecked {
366:                 ++i;
367:             }
368:         }
369: 
370:         
371:         $.usdcToken.safeTransfer(recipient, totalUsdcTaken);
372:         
373:         if (
374:             !partialMatchingAllowed && totalUsdcTaken != amountUsdcToTakeInNativeDecimals
375:                 || totalUsdcTaken == 0
376:         ) {
377:             revert AmountTooLow();
378:         }
379: 
380:         return ((amountUsdcToTakeInNativeDecimals - totalUsdcTaken), totalUsd0Provided); // <= FOUND
381:     }

[NonCritical-17] Constants should be on the left side of the comparison

Resolution

Putting constants on the left side of a comparison operator like == or < is a best practice known as "Yoda conditions", which can help prevent accidental assignment instead of comparison. In some programming languages, if a variable is mistakenly put on the left with a single = instead of ==, it assigns the constant's value to the variable without any compiler error. However, doing this with the constant on the left would generate an error, as constants cannot be assigned values. Although Solidity's static typing system prevents accidental assignments within conditionals, adopting this practice enhances code readability and consistency, especially when developers are working across multiple languages that support this convention.

Num of instances: 19

Findings

Click to show findings

['60']

60:         if (answer <= 0 || updatedAt == 0 || block.timestamp > updatedAt + timeout)  // <= FOUND

['418']

418:        if (amountInToken == 0)  // <= FOUND

['433']

433:         if (wadQuoteInUSD == 0)  // <= FOUND

['565']

565:         if (returnedCollateral == 0)  // <= FOUND

['595']

595:        if (amountInTokenDecimals == 0)  // <= FOUND

['601']

601:         if (orderIdsToTake.length == 0)  // <= FOUND

['205']

205:         if (amount == 0)  // <= FOUND

['320']

320:        if (amountUsdcToTakeInNativeDecimals == 0)  // <= FOUND

['373']

373:         if (
374:             !partialMatchingAllowed && totalUsdcTaken != amountUsdcToTakeInNativeDecimals
375:                 || totalUsdcTaken == 0 // <= FOUND
376:         ) 

['92']

92:         if (IERC20Metadata(rwa).decimals() == 0)  // <= FOUND

['205']

205:        if (amount == 0)  // <= FOUND

['143']

143:         if (bytes(name).length == 0)  // <= FOUND

['147']

147:         if (bytes(symbol).length == 0)  // <= FOUND

['81']

81:         if (answer <= 0 || block.timestamp - timestamp > timeout)  // <= FOUND

['610']

610:         if (approval.deadline != 0 && approval.v != 0 && approval.r != 0 && approval.s != 0)  // <= FOUND

['353']

353:                if (order.tokenAmount == 0)  // <= FOUND

['541']

541:         if (stableFee > 0)  // <= FOUND

['637']

637:         if (wadRwaNotTakenInUSD > 0)  // <= FOUND

['533']

533:         if (tokenDecimals < 18)  // <= FOUND

[NonCritical-18] Initialize functions do not emit an event

Resolution

Emitting an event within initializer functions in Solidity is a best practice for providing transparency and traceability. Initializer functions set the initial state and values of an upgradeable contract. Emitting an event during initialization allows anyone to verify and audit the initial state of the contract via the transaction logs. This can be particularly useful for verifying the parameters set during initialization, tracking the contract's deployment, and troubleshooting or debugging. Therefore, developers should include an event emission in their initializer functions, providing a clear record of the contract's initialization and enhancing the contract's transparency and security.

Num of instances: 9

Findings

Click to show findings

['38']

38:     function initialize(address registryContract) public initializer { // <= FOUND
39:         __AbstractOracle_init_unchained(registryContract);
40:     }

['260']

260:     function initialize(address _registryContract, uint256 _redeemFee) public initializer { // <= FOUND
261:         
262:         if (_redeemFee > MAX_REDEEM_FEE) {
263:             revert RedeemFeeTooBig();
264:         }
265: 
266:         if (_registryContract == address(0)) {
267:             revert NullContract();
268:         }
269: 
270:         __EIP712_init_unchained("DaoCollateral", "1");
271:         __Nonces_init_unchained();
272:         __Pausable_init_unchained();
273:         __ReentrancyGuard_init_unchained();
274: 
275:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
276:         $.redeemFee = _redeemFee;
277: 
278:         IRegistryContract registryContract = IRegistryContract(_registryContract);
279:         $.registryAccess = IRegistryAccess(registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
280: 
281:         $.treasury = address(registryContract.getContract(CONTRACT_TREASURY));
282:         $.tokenMapping = ITokenMapping(registryContract.getContract(CONTRACT_TOKEN_MAPPING));
283:         $.usd0 = IUsd0(registryContract.getContract(CONTRACT_USD0));
284: 
285:         $.oracle = IOracle(registryContract.getContract(CONTRACT_ORACLE));
286: 
287:         $.swapperEngine = ISwapperEngine(registryContract.getContract(CONTRACT_SWAPPER_ENGINE));
288:     }

['27']

27:     function initialize(address deployer) public initializer { // <= FOUND
28:         
29:         if (deployer == address(0)) {
30:             revert NullAddress();
31:         }
32:         __AccessControl_init_unchained();
33:         __AccessControlDefaultAdminRules_init_unchained(
34:             3 days,
35:             deployer 
36:         );
37:     }

['67']

67:     function initialize(address registryAccess_) public initializer { // <= FOUND
68:         if (registryAccess_ == address(0)) {
69:             revert NullAddress();
70:         }
71: 
72:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
73:         $._registryAccess = registryAccess_;
74:     }

['124']

124:     function initialize(address registryContract) public initializer { // <= FOUND
125:         if (registryContract == address(0)) {
126:             revert NullContract();
127:         }
128:         __Pausable_init_unchained();
129:         __ReentrancyGuard_init_unchained();
130:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
131:         $.registryContract = IRegistryContract(registryContract);
132:         $.registryAccess = IRegistryAccess($.registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
133:         $.usdcToken = IERC20($.registryContract.getContract(CONTRACT_USDC));
134:         $.usd0 = IERC20($.registryContract.getContract(CONTRACT_USD0));
135:         $.nextOrderId = 1;
136:         $.oracle = IOracle($.registryContract.getContract(CONTRACT_ORACLE));
137:         $.minimumUSDCAmountProvided = MINIMUM_USDC_PROVIDED;
138:     }

['74']

74:     function initialize(address registryAccess, address registryContract) public initializer { // <= FOUND
75:         if (registryAccess == address(0) || registryContract == address(0)) {
76:             revert NullAddress();
77:         }
78: 
79:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
80:         $._registryAccess = IRegistryAccess(registryAccess);
81:         $._registryContract = IRegistryContract(registryContract);
82:     }

['67']

67:     function initialize(address registryContract_, string memory name_, string memory symbol_) // <= FOUND
68:         public
69:         initializer
70:     {
71:         
72:         __ERC20_init_unchained(name_, symbol_);
73:         
74:         __Pausable_init_unchained();
75:         
76:         __ERC20Permit_init_unchained(name_);
77:         
78:         __EIP712_init_unchained(name_, "1");
79:         
80:         if (registryContract_ == address(0)) {
81:             revert NullContract();
82:         }
83:         _usd0StorageV0().registryAccess = IRegistryAccess(
84:             IRegistryContract(registryContract_).getContract(CONTRACT_REGISTRY_ACCESS)
85:         );
86:     }

['108']

108:     function initialize( // <= FOUND
109:         address registryContract,
110:         string memory name_,
111:         string memory symbol_,
112:         uint256 startTime
113:     ) public initializer {
114:         _createUsd0PPCheck(name_, symbol_, startTime);
115: 
116:         __ERC20_init_unchained(name_, symbol_);
117:         __ERC20Permit_init_unchained(name_);
118:         __EIP712_init_unchained(name_, "1");
119:         __ReentrancyGuard_init_unchained();
120:         
121:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
122:         $.bondStart = startTime;
123:         $.registryContract = IRegistryContract(registryContract);
124:         $.usd0 = IERC20(IRegistryContract(registryContract).getContract(CONTRACT_USD0));
125:         $.registryAccess = IRegistryAccess(
126:             IRegistryContract(registryContract).getContract(CONTRACT_REGISTRY_ACCESS)
127:         );
128:     }

['56']

56:     function initialize(address registryContractAddress) public initializer { // <= FOUND
57:         __AbstractOracle_init_unchained(registryContractAddress);
58: 
59:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
60:         UsualOracleStorageV0 storage u = _usualOracleStorageV0();
61:         u.dataPublisher = IDataPublisher($.registryContract.getContract(CONTRACT_DATA_PUBLISHER));
62:     }

[NonCritical-19] Both immutable and constant state variables should be CONSTANT_CASE

Resolution

Make found instants CAPITAL_CASE

Num of instances: 8

Findings

Click to show findings

['112']

112: bytes32 public constant DaoCollateralStorageV0Location = // <= FOUND

['29']

29: bytes32 public constant RegistryContractStorageV0Location = // <= FOUND

['99']

99: bytes32 public constant SwapperEngineStorageV0Location = // <= FOUND

['39']

39: bytes32 public constant TokenMappingStorageV0Location = // <= FOUND

['41']

41: bytes32 public constant Usd0StorageV0Location = // <= FOUND

['79']

79: bytes32 public constant Usd0PPStorageV0Location = // <= FOUND

['31']

31: bytes32 public constant UsualOracleStorageV0Location = // <= FOUND

['61']

61: bytes32 public constant AbstractOracleStorageV0Location = // <= FOUND

[NonCritical-20] Use of non-named numeric constants

Resolution

Magic numbers should be avoided in Solidity code to enhance readability, maintainability, and reduce the likelihood of errors. Magic numbers are hard-coded values with no clear meaning or context, which can create confusion and make the code harder to understand for developers. Using well-defined constants or variables with descriptive names instead of magic numbers not only clarifies the purpose and significance of the value but also simplifies code updates and modifications.

Num of instances: 5

Findings

Click to show findings

['533']

533:         
534:         if (tokenDecimals < 18) { // <= FOUND

['39']

39:         return tokenAmountToDecimals(tokenAmount, tokenDecimals, 18); // <= FOUND

['83']

83:         return Math.mulDiv(wadStableAmount, 10 ** tokenDecimals, wadPrice, Math.Rounding.Floor); // <= FOUND

['95']

95:         return tokenAmountToDecimals(wadAmount, 18, targetDecimals); // <= FOUND

['33']

33:         __AccessControlDefaultAdminRules_init_unchained(
34:             3 days, // <= FOUND
35:             deployer 
36:         );

[NonCritical-21] Redundant else statement

Num of instances: 1

Findings

Click to show findings

[]

16:     function tokenAmountToDecimals(uint256 tokenAmount, uint8 tokenDecimals, uint8 targetDecimals)
17:         internal
18:         pure
19:         returns (uint256)
20:     {
21:         if (tokenDecimals < targetDecimals) {
22:             return tokenAmount * (10 ** uint256(targetDecimals - tokenDecimals));
23:         } else if (tokenDecimals > targetDecimals) {
24:             return tokenAmount / (10 ** uint256(tokenDecimals - targetDecimals));
25:         } else {
26:             return tokenAmount;
27:         }
28:     }

[NonCritical-22] Event emit should emit a parameter

Resolution

Events in Solidity offer valuable insight into the contract's execution as they log specific instances of state changes or value transfers. However, if events do not include any parameters, their usefulness can be significantly reduced. Events without parameters can provide limited information, mainly signaling that a specific operation occurred, but lacking the context of what exactly changed. It's generally recommended to include relevant parameters, such as state changes or value modifications, in the emitted events.

Num of instances: 5

Findings

Click to show findings

['328']

328:         emit SwapPaused(); // <= FOUND

['338']

338:         emit CBRDeactivated(); // <= FOUND

['359']

359:         emit RedeemPaused(); // <= FOUND

['368']

368:         emit RedeemUnPaused(); // <= FOUND

['386']

386:         emit SwapUnPaused(); // <= FOUND

[NonCritical-23] Unused structs present

Resolution

If these serve no purpose, they should be safely removed

Num of instances: 1

Findings

Click to show findings

['9']

9:     struct Reward { // <= FOUND
10:         address token;
11:         address distributor;
12:         uint256 period_finish;
13:         uint256 rate;
14:         uint256 last_update;
15:         uint256 integral;
16:     }

[NonCritical-24] Empty bytes check is missing

Resolution

When developing smart contracts in Solidity, it's crucial to validate the inputs of your functions. This includes ensuring that the bytes parameters are not empty, especially when they represent crucial data such as addresses, identifiers, or raw data that the contract needs to process.

Missing empty bytes checks can lead to unexpected behaviour in your contract. For instance, certain operations might fail, produce incorrect results, or consume unnecessary gas when performed with empty bytes. Moreover, missing input validation can potentially expose your contract to malicious activity, including exploitation of unhandled edge cases.

To mitigate these issues, always validate that bytes parameters are not empty when the logic of your contract requires it.

Num of instances: 8

Findings

Click to show findings

['14']

14:     function onlyMatchingRole(IRegistryAccess registryAccess, bytes32 role) internal view {
15:         if (!registryAccess.hasRole(role, msg.sender)) {
16:             revert NotAuthorized();
17:         }
18:     }

['685']

685:     function swapWithPermit(
686:         address rwaToken,
687:         uint256 amount,
688:         uint256 minAmountOut,
689:         uint256 deadline,
690:         uint8 v,
691:         bytes32 r,
692:         bytes32 s
693:     ) external {
694:         
695:         try IERC20Permit(rwaToken).permit(msg.sender, address(this), amount, deadline, v, r, s) {}
696:             catch {} 
697:         swap(rwaToken, amount, minAmountOut);
698:     }

['39']

39:     function setRoleAdmin(bytes32 role, bytes32 adminRole) external {
40:         if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
41:             revert NotAuthorized();
42:         }
43:         _setRoleAdmin(role, adminRole);
44:     }

['83']

83:     function setContract(bytes32 name, address contractAddress) external {
84:         
85:         if (contractAddress == address(0)) {
86:             revert NullAddress();
87:         }
88:         
89:         if (name == bytes32(0)) {
90:             revert InvalidName();
91:         }
92: 
93:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
94:         
95:         if (!IRegistryAccess($._registryAccess).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
96:             revert NotAuthorized();
97:         }
98: 
99:         $._contracts[name] = contractAddress;
100:         emit SetContract(name, contractAddress);
101:     }

['106']

106:     function getContract(bytes32 name) external view returns (address) {
107:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
108:         address _contract = $._contracts[name];
109:         
110:         if (_contract == address(0)) {
111:             revert NullAddress();
112:         }
113: 
114:         return _contract;
115:     }

['266']

266:     function depositUSDCWithPermit(
267:         uint256 amountToDeposit,
268:         uint256 deadline,
269:         uint8 v,
270:         bytes32 r,
271:         bytes32 s
272:     ) external nonReentrant whenNotPaused {
273:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
274:         try IERC20Permit(address($.usdcToken)).permit(
275:             msg.sender, address(this), amountToDeposit, deadline, v, r, s
276:         ) {} catch {} 
277:         _depositUSDC($, amountToDeposit);
278:     }

['408']

408:     function provideUsd0ReceiveUSDCWithPermit(
409:         address recipient,
410:         uint256 amountUsdcToTakeInNativeDecimals,
411:         uint256[] memory orderIdsToTake,
412:         bool partialMatchingAllowed,
413:         uint256 usd0ToPermit,
414:         uint256 deadline,
415:         uint8 v,
416:         bytes32 r,
417:         bytes32 s
418:     ) external nonReentrant whenNotPaused returns (uint256) {
419:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
420:         uint256 usdcWadPrice = _getUsdcWadPrice();
421:         uint256 requiredUsd0Amount =
422:             _getUsd0WadEquivalent(amountUsdcToTakeInNativeDecimals, usdcWadPrice);
423:         
424:         if ($.usd0.balanceOf(msg.sender) < requiredUsd0Amount || usd0ToPermit < requiredUsd0Amount)
425:         {
426:             revert InsufficientUSD0Balance();
427:         }
428:         try IERC20Permit(address($.usd0)).permit(
429:             msg.sender, address(this), usd0ToPermit, deadline, v, r, s
430:         ) {} catch {} 
431:         (uint256 unmatchedUsdcAmount,) = _provideUsd0ReceiveUSDC(
432:             recipient,
433:             amountUsdcToTakeInNativeDecimals,
434:             orderIdsToTake,
435:             partialMatchingAllowed,
436:             usdcWadPrice
437:         );
438:         return unmatchedUsdcAmount;
439:     }

['189']

189:     function mintWithPermit(uint256 amountUsd0, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
190:         external
191:     {
192:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
193: 
194:         try IERC20Permit(address($.usd0)).permit(
195:             msg.sender, address(this), amountUsd0, deadline, v, r, s
196:         ) {} catch {} 
197: 
198:         mint(amountUsd0);
199:     }

[NonCritical-25] Return bool not explicit

Resolution

In Solidity, when designing functions that return boolean values, it's crucial for clarity and maintainability to explicitly handle both true and false return scenarios. If a function is intended to return true under certain conditions, it should also explicitly return false when these conditions are not met, and vice versa. This approach eliminates ambiguity and makes the code's intent more transparent. Explicitly handling all possible outcomes of a boolean function ensures that future modifications or extensions of the contract do not unintentionally alter its logic. It contributes to better readability, easier debugging, and reduces the risk of bugs related to unintended fall-through cases.

Num of instances: 1

Findings

Click to show findings

['85']

85:     function addUsd0Rwa(address rwa) external returns (bool) {
86:         if (rwa == address(0)) {
87:             revert NullAddress();
88:         }
89:         
90:         
91:         
92:         if (IERC20Metadata(rwa).decimals() == 0) {
93:             revert Invalid();
94:         }
95: 
96:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
97:         $._registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
98: 
99:         
100:         if ($.isUsd0Collateral[rwa]) revert SameValue();
101:         $.isUsd0Collateral[rwa] = true;
102:         
103:         ++$._usd0ToRwaLastId;
104:         if ($._usd0ToRwaLastId > MAX_RWA_COUNT) {
105:             revert TooManyRWA();
106:         }
107:         $.USD0Rwas[$._usd0ToRwaLastId] = rwa;
108:         emit AddUsd0Rwa(rwa, $._usd0ToRwaLastId);
109:         return true; // <= FOUND
110:     }

[NonCritical-26] Do not use underscore at the end of variable name

Resolution

Adopting a consistent and clear naming convention enhances code readability and maintainability. In Solidity, appending an underscore at the end of a variable name is unconventional and can lead to confusion. It is generally advisable to stick to accepted naming practices to promote ease of understanding and use.

Num of instances: 2

Findings

Click to show findings

['68']

68:         if (registryAccess_ == address(0)) { // <= FOUND

['80']

80:         
81:         if (registryContract_ == address(0)) { // <= FOUND

[NonCritical-27] Cyclomatic complexity in functions

Resolution

Cyclomatic complexity is a software metric used to measure the complexity of a program. It quantifies the number of linearly independent paths through a program's source code, giving an idea of how complex the control flow is. High cyclomatic complexity may indicate a higher risk of defects and can make the code harder to understand, test, and maintain. It often suggests that a function or method is trying to do too much, and a refactor might be needed. By breaking down complex functions into smaller, more focused pieces, you can improve readability, ease of testing, and overall maintainability.

Num of instances: 8

Findings

Click to show findings

[]

413:     function _swapCheckAndGetUSDQuote(address rwaToken, uint256 amountInToken)
414:         internal
415:         view
416:         returns (uint256 wadQuoteInUSD)
417:     {
418:         if (amountInToken == 0) {
419:             revert AmountIsZero();
420:         }
421: 
422:         
423:         if (amountInToken > type(uint128).max) {
424:             revert AmountTooBig();
425:         }
426: 
427:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
428:         if (!$.tokenMapping.isUsd0Collateral(rwaToken)) {
429:             revert InvalidToken();
430:         }
431:         wadQuoteInUSD = _getQuoteInUsd(amountInToken, rwaToken);
432:         
433:         if (wadQuoteInUSD == 0) {
434:             revert AmountTooLow();
435:         }
436:     }

[]

587:     function _swapRWAtoStbc(
588:         address caller,
589:         address rwaToken,
590:         uint256 amountInTokenDecimals,
591:         bool partialMatching,
592:         uint256[] calldata orderIdsToTake,
593:         Approval calldata approval
594:     ) internal returns (uint256 matchedAmountInTokenDecimals, uint256 matchedAmountInUSD) {
595:         if (amountInTokenDecimals == 0) {
596:             revert AmountIsZero();
597:         }
598:         if (amountInTokenDecimals > type(uint128).max) {
599:             revert AmountTooBig();
600:         }
601:         if (orderIdsToTake.length == 0) {
602:             revert NoOrdersIdsProvided();
603:         }
604:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
605:         if (!$.tokenMapping.isUsd0Collateral(rwaToken)) {
606:             revert InvalidToken();
607:         }
608: 
609:         
610:         if (approval.deadline != 0 && approval.v != 0 && approval.r != 0 && approval.s != 0) {
611:             
612:             try IERC20Permit(rwaToken).permit(
613:                 caller,
614:                 address(this),
615:                 amountInTokenDecimals,
616:                 approval.deadline,
617:                 approval.v,
618:                 approval.r,
619:                 approval.s
620:             ) {} catch {} 
621:         }
622: 
623:         
624:         IERC20Metadata(rwaToken).safeTransferFrom(caller, address(this), amountInTokenDecimals);
625:         
626:         uint256 wadRwaQuoteInUSD = _getQuoteInUsd(amountInTokenDecimals, rwaToken);
627:         
628:         $.usd0.mint(address(this), wadRwaQuoteInUSD);
629:         if (!IERC20($.usd0).approve(address($.swapperEngine), wadRwaQuoteInUSD)) {
630:             revert ApprovalFailed();
631:         }
632:         
633:         uint256 wadRwaNotTakenInUSD =
634:             $.swapperEngine.swapUsd0(caller, wadRwaQuoteInUSD, orderIdsToTake, partialMatching);
635: 
636:         
637:         if (wadRwaNotTakenInUSD > 0) {
638:             if (!IERC20($.usd0).approve(address($.swapperEngine), 0)) {
639:                 revert ApprovalFailed();
640:             }
641:             $.usd0.burnFrom(address(this), wadRwaNotTakenInUSD);
642: 
643:             
644:             uint256 rwaTokensToReturn = _getQuoteInToken(wadRwaNotTakenInUSD, rwaToken);
645: 
646:             
647:             IERC20Metadata(rwaToken).safeTransfer(caller, rwaTokensToReturn);
648: 
649:             matchedAmountInTokenDecimals = amountInTokenDecimals - rwaTokensToReturn;
650:         } else {
651:             matchedAmountInTokenDecimals = amountInTokenDecimals;
652:         }
653: 
654:         
655:         IERC20Metadata(rwaToken).safeTransfer($.treasury, matchedAmountInTokenDecimals);
656: 
657:         matchedAmountInUSD = wadRwaQuoteInUSD - wadRwaNotTakenInUSD;
658:         emit Swap(caller, rwaToken, matchedAmountInTokenDecimals, matchedAmountInUSD);
659: 
660:         return (matchedAmountInTokenDecimals, matchedAmountInUSD);
661:     }

[]

701:     function redeem(address rwaToken, uint256 amount, uint256 minAmountOut)
702:         external
703:         nonReentrant
704:         whenRedeemNotPaused
705:         whenNotPaused
706:     {
707:         
708:         if (amount == 0) {
709:             revert AmountIsZero();
710:         }
711: 
712:         
713:         if (!_daoCollateralStorageV0().tokenMapping.isUsd0Collateral(rwaToken)) {
714:             revert InvalidToken();
715:         }
716:         uint256 stableFee = _transferFee(amount, rwaToken);
717:         uint256 returnedCollateral =
718:             _burnStableTokenAndTransferCollateral(rwaToken, amount, stableFee);
719:         
720:         if (returnedCollateral < minAmountOut) {
721:             revert AmountTooLow();
722:         }
723:         emit Redeem(msg.sender, rwaToken, amount, returnedCollateral, stableFee);
724:     }

[]

762:     function swapRWAtoStbcIntent(
763:         uint256[] calldata orderIdsToTake,
764:         Approval calldata approval,
765:         Intent calldata intent,
766:         bool partialMatching
767:     ) external nonReentrant whenNotPaused whenSwapNotPaused {
768:         if (block.timestamp > intent.deadline) {
769:             revert ExpiredSignature(intent.deadline);
770:         }
771:         if (approval.deadline != intent.deadline) {
772:             revert InvalidDeadline(approval.deadline, intent.deadline);
773:         }
774: 
775:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
776:         $.registryAccess.onlyMatchingRole(INTENT_MATCHING_ROLE);
777: 
778:         uint256 nonce = _useNonce(intent.recipient);
779: 
780:         
781:         bytes32 structHash = keccak256(
782:             abi.encode(
783:                 INTENT_TYPE_HASH,
784:                 intent.recipient,
785:                 intent.rwaToken,
786:                 intent.amountInTokenDecimals,
787:                 nonce,
788:                 intent.deadline
789:             )
790:         );
791: 
792:         bytes32 hash = _hashTypedDataV4(structHash);
793: 
794:         if (!SignatureChecker.isValidSignatureNow(intent.recipient, hash, intent.signature)) {
795:             revert InvalidSigner(intent.recipient);
796:         }
797:         (uint256 amountInTokenDecimals, uint256 amountInUSD) = _swapRWAtoStbc(
798:             intent.recipient,
799:             intent.rwaToken,
800:             intent.amountInTokenDecimals,
801:             partialMatching,
802:             orderIdsToTake,
803:             approval
804:         );
805:         emit IntentMatched(
806:             intent.recipient, nonce, intent.rwaToken, amountInTokenDecimals, amountInUSD
807:         );
808:     }

['83']

83:     function setContract(bytes32 name, address contractAddress) external { // <= FOUND
84:         
85:         if (contractAddress == address(0)) {
86:             revert NullAddress();
87:         }
88:         
89:         if (name == bytes32(0)) {
90:             revert InvalidName();
91:         }
92: 
93:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
94:         
95:         if (!IRegistryAccess($._registryAccess).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
96:             revert NotAuthorized();
97:         }
98: 
99:         $._contracts[name] = contractAddress;
100:         emit SetContract(name, contractAddress);
101:     }

[]

313:     function _provideUsd0ReceiveUSDC( 
314:         address recipient,
315:         uint256 amountUsdcToTakeInNativeDecimals,
316:         uint256[] memory orderIdsToTake,
317:         bool partialMatchingAllowed,
318:         uint256 usdcWadPrice
319:     ) internal returns (uint256 unmatchedUsdcAmount, uint256 totalUsd0Provided) {
320:         if (amountUsdcToTakeInNativeDecimals == 0) {
321:             
322:             revert AmountIsZero();
323:         }
324:         if (orderIdsToTake.length == 0) {
325:             revert NoOrdersIdsProvided();
326:         }
327: 
328:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
329: 
330:         uint256 totalUsdcTaken = 0;
331: 
332:         for (
333:             uint256 i;
334:             i < orderIdsToTake.length && totalUsdcTaken < amountUsdcToTakeInNativeDecimals;
335:         ) {
336:             uint256 orderId = orderIdsToTake[i];
337:             UsdcOrder storage order = $.orders[orderId];
338: 
339:             if (order.active) {
340:                 uint256 remainingAmountToTake = amountUsdcToTakeInNativeDecimals - totalUsdcTaken;
341:                 
342:                 uint256 amountOfUsdcFromOrder = order.tokenAmount > remainingAmountToTake
343:                     ? remainingAmountToTake
344:                     : order.tokenAmount;
345: 
346:                 
347:                 
348:                 
349: 
350:                 order.tokenAmount -= amountOfUsdcFromOrder;
351:                 totalUsdcTaken += amountOfUsdcFromOrder;
352: 
353:                 if (order.tokenAmount == 0) {
354:                     order.active = false;
355:                 }
356: 
357:                 uint256 usd0Amount = _getUsd0WadEquivalent(amountOfUsdcFromOrder, usdcWadPrice);
358:                 totalUsd0Provided += usd0Amount;
359:                 
360:                 $.usd0.safeTransferFrom(msg.sender, order.requester, usd0Amount);
361: 
362:                 emit OrderMatched(order.requester, msg.sender, orderId, amountOfUsdcFromOrder);
363:             }
364: 
365:             unchecked {
366:                 ++i;
367:             }
368:         }
369: 
370:         
371:         $.usdcToken.safeTransfer(recipient, totalUsdcTaken);
372:         
373:         if (
374:             !partialMatchingAllowed && totalUsdcTaken != amountUsdcToTakeInNativeDecimals
375:                 || totalUsdcTaken == 0
376:         ) {
377:             revert AmountTooLow();
378:         }
379: 
380:         return ((amountUsdcToTakeInNativeDecimals - totalUsdcTaken), totalUsd0Provided);
381:     }

['85']

85:     function addUsd0Rwa(address rwa) external returns (bool) { // <= FOUND
86:         if (rwa == address(0)) {
87:             revert NullAddress();
88:         }
89:         
90:         
91:         
92:         if (IERC20Metadata(rwa).decimals() == 0) {
93:             revert Invalid();
94:         }
95: 
96:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
97:         $._registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
98: 
99:         
100:         if ($.isUsd0Collateral[rwa]) revert SameValue();
101:         $.isUsd0Collateral[rwa] = true;
102:         
103:         ++$._usd0ToRwaLastId;
104:         if ($._usd0ToRwaLastId > MAX_RWA_COUNT) {
105:             revert TooManyRWA();
106:         }
107:         $.USD0Rwas[$._usd0ToRwaLastId] = rwa;
108:         emit AddUsd0Rwa(rwa, $._usd0ToRwaLastId);
109:         return true;
110:     }

[]

134:     function _createUsd0PPCheck(string memory name, string memory symbol, uint256 startTime)
135:         internal
136:         view
137:     {
138:         
139:         if (startTime < block.timestamp) {
140:             revert BeginInPast();
141:         }
142:         
143:         if (bytes(name).length == 0) {
144:             revert InvalidName();
145:         }
146:         
147:         if (bytes(symbol).length == 0) {
148:             revert InvalidSymbol();
149:         }
150:     }

[NonCritical-28] Missing events in sensitive functions

Resolution

Sensitive setter functions in smart contracts often alter critical state variables. Without events emitted in these functions, external observers or dApps cannot easily track or react to these state changes. Missing events can obscure contract activity, hampering transparency and making integration more challenging. To resolve this, incorporate appropriate event emissions within these functions. Events offer an efficient way to log crucial changes, aiding in real-time tracking and post-transaction verification.

Num of instances: 4

Findings

Click to show findings

['39']

39:     function setRoleAdmin(bytes32 role, bytes32 adminRole) external { // <= FOUND
40:         if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
41:             revert NotAuthorized();
42:         }
43:         _setRoleAdmin(role, adminRole);
44:     }

['212']

212:     function updateMinimumUSDCAmountProvided(uint256 minimumUSDCAmount) external { // <= FOUND
213:         if (minimumUSDCAmount < ONE_USDC) {
214:             
215:             revert AmountTooLow();
216:         }
217:         _requireOnlyAdmin();
218:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
219:         $.minimumUSDCAmountProvided = minimumUSDCAmount;
220:     }

['166']

166:     function _update(address from, address to, uint256 amount) // <= FOUND
167:         internal
168:         virtual
169:         override(ERC20PausableUpgradeable, ERC20Upgradeable)
170:     {
171:         Usd0StorageV0 storage $ = _usd0StorageV0();
172:         if ($.isBlacklisted[from] || $.isBlacklisted[to]) {
173:             revert Blacklisted();
174:         }
175:         super._update(from, to, amount);
176:     }

['201']

201:     function _update(address sender, address recipient, uint256 amount) // <= FOUND
202:         internal
203:         override(ERC20PausableUpgradeable, ERC20Upgradeable)
204:     {
205:         if (amount == 0) {
206:             revert AmountIsZero();
207:         }
208:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
209:         IUsd0 usd0 = IUsd0(address($.usd0));
210:         if (usd0.isBlacklisted(sender) || usd0.isBlacklisted(recipient)) {
211:             revert Blacklisted();
212:         }
213:         
214:         super._update(sender, recipient, amount);
215:     }

[NonCritical-29] No limit when setting fees in initializer

Num of instances: 1

Findings

Click to show findings

['260']

260:     function initialize(address _registryContract, uint256 _redeemFee) public initializer {
261:         
262:         if (_redeemFee > MAX_REDEEM_FEE) {
263:             revert RedeemFeeTooBig();
264:         }
265: 
266:         if (_registryContract == address(0)) {
267:             revert NullContract();
268:         }
269: 
270:         __EIP712_init_unchained("DaoCollateral", "1");
271:         __Nonces_init_unchained();
272:         __Pausable_init_unchained();
273:         __ReentrancyGuard_init_unchained();
274: 
275:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
276:         $.redeemFee = _redeemFee; // <= FOUND
277: 
278:         IRegistryContract registryContract = IRegistryContract(_registryContract);
279:         $.registryAccess = IRegistryAccess(registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
280: 
281:         $.treasury = address(registryContract.getContract(CONTRACT_TREASURY));
282:         $.tokenMapping = ITokenMapping(registryContract.getContract(CONTRACT_TOKEN_MAPPING));
283:         $.usd0 = IUsd0(registryContract.getContract(CONTRACT_USD0));
284: 
285:         $.oracle = IOracle(registryContract.getContract(CONTRACT_ORACLE));
286: 
287:         $.swapperEngine = ISwapperEngine(registryContract.getContract(CONTRACT_SWAPPER_ENGINE));
288:     }

[NonCritical-30] Pure function storage pointer bug

Resolution

In Solidity, a function marked as pure is expected not to read from or modify the state of the blockchain, meaning it shouldn't access or alter any state variables. However, a common oversight occurs when such functions inadvertently use storage pointers. A storage pointer in Solidity refers directly to a state variable, and a pure function using these pointers may end up interacting with the contract's state, contradicting its pure designation. This can lead to subtle bugs because the compiler doesn't flag this misuse, as it expects no state interaction in pure functions. The resolution is to thoroughly review pure functions to ensure they don't use storage pointers, or if necessary, reclassify them as view functions if they must read (but not modify) the contract's state. This distinction is crucial for both accurate representation of the function's behavior and for maintaining contract integrity and security.

Num of instances: 8

Findings

Click to show findings

['66']

66:     function _abstractOracleStorageV0() internal pure returns (AbstractOracleStorageV0 storage $) { // <= FOUND
67:         bytes32 position = AbstractOracleStorageV0Location;
68:         
69:         assembly {
70:             $.slot := position
71:         }
72:     }

['249']

249:     function _daoCollateralStorageV0() internal pure returns (DaoCollateralStorageV0 storage $) { // <= FOUND
250:         bytes32 position = DaoCollateralStorageV0Location;
251:         
252:         assembly {
253:             $.slot := position
254:         }
255:     }

['34']

34:     function _registryContractStorageV0()
35:         internal
36:         pure
37:         returns (RegistryContractStorageV0 storage $)
38:     {
39:         bytes32 position = RegistryContractStorageV0Location;
40:         
41:         assembly {
42:             $.slot := position
43:         }
44:     }

['104']

104:     function _swapperEngineStorageV0() internal pure returns (SwapperEngineStorageV0 storage $) { // <= FOUND
105:         bytes32 position = SwapperEngineStorageV0Location;
106:         
107:         assembly {
108:             $.slot := position
109:         }
110:     }

['44']

44:     function _tokenMappingStorageV0() private pure returns (TokenMappingStorageV0 storage $) { // <= FOUND
45:         bytes32 position = TokenMappingStorageV0Location;
46:         
47:         assembly {
48:             $.slot := position
49:         }
50:     }

['46']

46:     function _usd0StorageV0() internal pure returns (Usd0StorageV0 storage $) { // <= FOUND
47:         bytes32 position = Usd0StorageV0Location;
48:         
49:         assembly {
50:             $.slot := position
51:         }
52:     }

['84']

84:     function _usd0ppStorageV0() private pure returns (Usd0PPStorageV0 storage $) { // <= FOUND
85:         bytes32 position = Usd0PPStorageV0Location;
86:         
87:         assembly {
88:             $.slot := position
89:         }
90:     }

['36']

36:     function _usualOracleStorageV0() private pure returns (UsualOracleStorageV0 storage $) { // <= FOUND
37:         bytes32 position = UsualOracleStorageV0Location;
38:         
39:         assembly {
40:             $.slot := position
41:         }
42:     }

[NonCritical-31] Try catch statement without human readable error

Resolution

In Solidity, the try-catch statement is used for handling exceptions in external function calls and contract creation. However, when a try-catch block doesn't include a catch for specific human-readable errors (using catch Error(string memory reason)), it can miss catching exceptions that provide explanatory error messages. This lack of detailed error handling could hinder debugging and obscure the reasons behind transaction failures. To address this, it's recommended to include a catch block specifically for Error to capture and handle these descriptive error messages effectively. This practice enhances the contract's robustness by providing clearer insights into why certain operations fail, thereby improving maintainability and troubleshooting.

Num of instances: 5

Findings

Click to show findings

['612']

612:             
613:             try IERC20Permit(rwaToken).permit(
614:                 caller,
615:                 address(this),
616:                 amountInTokenDecimals,
617:                 approval.deadline,
618:                 approval.v,
619:                 approval.r,
620:                 approval.s
621:             ) {} catch {}  // <= FOUND

['696']

696:             catch {}  // <= FOUND

['274']

274:         try IERC20Permit(address($.usdcToken)).permit(
275:             msg.sender, address(this), amountToDeposit, deadline, v, r, s
276:         ) {} catch {}  // <= FOUND

['428']

428:         try IERC20Permit(address($.usd0)).permit(
429:             msg.sender, address(this), usd0ToPermit, deadline, v, r, s
430:         ) {} catch {}  // <= FOUND

['194']

194:         try IERC20Permit(address($.usd0)).permit(
195:             msg.sender, address(this), amountUsd0, deadline, v, r, s
196:         ) {} catch {}  // <= FOUND

[NonCritical-32] Avoid declaring variables with the names of defined functions within the project

Resolution

Having such variables can create confusion in both developers and in users of the project. Consider renaming these variables to improve code clarity.

Num of instances: 6

Findings

Click to show findings

['93']

93:         
94:         uint256 cbrCoef; // <= FOUND

['91']

91:         
92:         uint256 redeemFee; // <= FOUND

['85']

85:         uint256 minimumUSDCAmountProvided; // <= FOUND

['89']

89:         
90:         bool isCBROn; // <= FOUND

['158']

158:     
160:     event CBRActivated(uint256 cbrCoef); // <= FOUND

['165']

165:     
167:     event RedeemFeeUpdated(uint256 redeemFee); // <= FOUND

[NonCritical-33] Constructors should emit an event

Resolution

Emitting an event in a constructor of a smart contract provides transparency and traceability in blockchain applications. This event logs the contract’s creation, aiding in monitoring and verifying contract deployment. Although constructors are executed only once, the emitted event ensures the contract's initialization is recorded on the blockchain.

Num of instances: 4

Findings

Click to show findings

['49']

49:     constructor() { // <= FOUND
50:         _disableInitializers();
51:     }

['49']

49:     constructor() { // <= FOUND
50:         _disableInitializers();
51:     }

['49']

49:     constructor() { // <= FOUND
50:         _disableInitializers();
51:     }

['49']

49:     constructor() { // <= FOUND
50:         _disableInitializers();
51:     }

[NonCritical-34] Events should have parameters

Resolution

In Solidity, events are crucial for logging and informing external consumers about smart contract activities. Using parameters in events is essential for conveying detailed information about these activities. Parameters enable developers to specify what data is emitted, facilitating efficient and targeted data retrieval by front-end applications or off-chain monitoring tools. Well-defined event parameters improve transparency, enable better data analysis, and enhance the contract's interface with the external world, making interactions with the blockchain more informative and accessible.

Num of instances: 5

Findings

Click to show findings

['145']

145:     
146:     event RedeemPaused(); // <= FOUND

['148']

148:     
149:     event RedeemUnPaused(); // <= FOUND

['151']

151:     
152:     event SwapPaused(); // <= FOUND

['154']

154:     
155:     event SwapUnPaused(); // <= FOUND

['161']

161:     
162:     event CBRDeactivated(); // <= FOUND

[NonCritical-35] Avoid arithmetic directly within array indices

Resolution

Avoiding arithmetic directly within array indices, like test[i + 2], is recommended to prevent errors such as index out of bounds or incorrect data access. This practice enhances code readability and maintainability. Instead, calculate the index beforehand, store it in a variable, and then use that variable to access the array element. This approach reduces mistakes, especially in complex loops or when handling dynamic data, ensuring that each access is clear and verifiable. It's about keeping code clean and understandable, minimizing potential bugs.

Num of instances: 1

Findings

Click to show findings

['133']

133:             rwas[i - 1] = $.USD0Rwas[i]; // <= FOUND

[NonCritical-36] Memory-safe annotation missing

Resolution

Tagging assembly blocks as "memory safe" in Solidity helps maintainers and auditors quickly identify areas of the code less likely to introduce vulnerabilities due to improper memory access. This practice promotes safer contract development by clearly communicating assumptions about memory handling, aiding in the review process, and reducing the risk of introducing memory-related bugs. It's a part of best coding practices, enhancing code clarity and security, especially in complex, low-level operations where Solidity's safety checks are bypassed.

Num of instances: 1

Findings

Click to show findings

['39']

39:         assembly {
40:             $.slot := position
41:         }

[NonCritical-37] ERC777 tokens can introduce reentrancy risks

Resolution

ERC777 is an advanced token standard that introduces hooks, allowing operators to execute additional logic during transfers. While this feature offers greater flexibility, it also opens up the possibility of reentrancy attacks. Specifically, when tokens are sent, the receiving contract's tokensReceived hook gets called, and this external call can execute arbitrary code. An attacker can exploit this feature to re-enter the original function, potentially leading to double-spending or other types of financial manipulation.

To mitigate reentrancy risks with ERC777, it's crucial to adopt established security measures, such as utilizing reentrancy guards or following the check-effects-interactions pattern. Some developers opt to stick with the simpler ERC20 standard, which does not have these hooks, to minimize this risk. If you do choose to use ERC777, extreme caution and thorough auditing are advised to secure against potential reentrancy vulnerabilities.

Num of instances: 7

Findings

Click to show findings

['443']

443:     function _transferRWATokenAndMintStable(
444:         address rwaToken,
445:         uint256 amount,
446:         uint256 wadAmountInUSD
447:     ) internal {
448:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
449:         
450:         IERC20Metadata(address(rwaToken)).safeTransferFrom(msg.sender, $.treasury, amount); // <= FOUND
451:         
452:         $.usd0.mint(msg.sender, wadAmountInUSD);
453:     }

['552']

552:     function _burnStableTokenAndTransferCollateral(
553:         address rwaToken,
554:         uint256 stableAmount,
555:         uint256 stableFee
556:     ) internal returns (uint256 returnedCollateral) {
557:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
558:         
559:         uint256 burnedStable = stableAmount - stableFee;
560:         
561:         $.usd0.burnFrom(msg.sender, stableAmount);
562: 
563:         
564:         returnedCollateral = _getTokenAmountForAmountInUSD(burnedStable, rwaToken);
565:         if (returnedCollateral == 0) {
566:             revert AmountTooLow();
567:         }
568: 
569:         
570:         
571:         IERC20Metadata(rwaToken).safeTransferFrom($.treasury, msg.sender, returnedCollateral); // <= FOUND
572:     }

['587']

587:     function _swapRWAtoStbc(
588:         address caller,
589:         address rwaToken,
590:         uint256 amountInTokenDecimals,
591:         bool partialMatching,
592:         uint256[] calldata orderIdsToTake,
593:         Approval calldata approval
594:     ) internal returns (uint256 matchedAmountInTokenDecimals, uint256 matchedAmountInUSD) {
595:         if (amountInTokenDecimals == 0) {
596:             revert AmountIsZero();
597:         }
598:         if (amountInTokenDecimals > type(uint128).max) {
599:             revert AmountTooBig();
600:         }
601:         if (orderIdsToTake.length == 0) {
602:             revert NoOrdersIdsProvided();
603:         }
604:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
605:         if (!$.tokenMapping.isUsd0Collateral(rwaToken)) {
606:             revert InvalidToken();
607:         }
608: 
609:         
610:         if (approval.deadline != 0 && approval.v != 0 && approval.r != 0 && approval.s != 0) {
611:             
612:             try IERC20Permit(rwaToken).permit(
613:                 caller,
614:                 address(this),
615:                 amountInTokenDecimals,
616:                 approval.deadline,
617:                 approval.v,
618:                 approval.r,
619:                 approval.s
620:             ) {} catch {} 
621:         }
622: 
623:         
624:         IERC20Metadata(rwaToken).safeTransferFrom(caller, address(this), amountInTokenDecimals); // <= FOUND
625:         
626:         uint256 wadRwaQuoteInUSD = _getQuoteInUsd(amountInTokenDecimals, rwaToken);
627:         
628:         $.usd0.mint(address(this), wadRwaQuoteInUSD);
629:         if (!IERC20($.usd0).approve(address($.swapperEngine), wadRwaQuoteInUSD)) {
630:             revert ApprovalFailed();
631:         }
632:         
633:         uint256 wadRwaNotTakenInUSD =
634:             $.swapperEngine.swapUsd0(caller, wadRwaQuoteInUSD, orderIdsToTake, partialMatching);
635: 
636:         
637:         if (wadRwaNotTakenInUSD > 0) {
638:             if (!IERC20($.usd0).approve(address($.swapperEngine), 0)) {
639:                 revert ApprovalFailed();
640:             }
641:             $.usd0.burnFrom(address(this), wadRwaNotTakenInUSD);
642: 
643:             
644:             uint256 rwaTokensToReturn = _getQuoteInToken(wadRwaNotTakenInUSD, rwaToken);
645: 
646:             
647:             IERC20Metadata(rwaToken).safeTransfer(caller, rwaTokensToReturn); // <= FOUND
648: 
649:             matchedAmountInTokenDecimals = amountInTokenDecimals - rwaTokensToReturn;
650:         } else {
651:             matchedAmountInTokenDecimals = amountInTokenDecimals;
652:         }
653: 
654:         
655:         IERC20Metadata(rwaToken).safeTransfer($.treasury, matchedAmountInTokenDecimals); // <= FOUND
656: 
657:         matchedAmountInUSD = wadRwaQuoteInUSD - wadRwaNotTakenInUSD;
658:         emit Swap(caller, rwaToken, matchedAmountInTokenDecimals, matchedAmountInUSD);
659: 
660:         return (matchedAmountInTokenDecimals, matchedAmountInUSD);
661:     }

['236']

236:     function _depositUSDC(SwapperEngineStorageV0 storage $, uint256 amountToDeposit) internal { // <= FOUND
237:         if (amountToDeposit < $.minimumUSDCAmountProvided) {
238:             
239:             revert AmountTooLow();
240:         }
241:         if (IUsd0(address($.usd0)).isBlacklisted(msg.sender)) {
242:             revert NotAuthorized();
243:         }
244: 
245:         uint256 orderId = $.nextOrderId++;
246:         $.orders[orderId] =
247:             UsdcOrder({requester: msg.sender, tokenAmount: amountToDeposit, active: true});
248: 
249:         
250:         $.usdcToken.safeTransferFrom(msg.sender, address(this), amountToDeposit); // <= FOUND
251: 
252:         emit Deposit(msg.sender, orderId, amountToDeposit);
253:     }

['313']

313:     function _provideUsd0ReceiveUSDC( 
314:         address recipient,
315:         uint256 amountUsdcToTakeInNativeDecimals,
316:         uint256[] memory orderIdsToTake,
317:         bool partialMatchingAllowed,
318:         uint256 usdcWadPrice
319:     ) internal returns (uint256 unmatchedUsdcAmount, uint256 totalUsd0Provided) {
320:         if (amountUsdcToTakeInNativeDecimals == 0) {
321:             
322:             revert AmountIsZero();
323:         }
324:         if (orderIdsToTake.length == 0) {
325:             revert NoOrdersIdsProvided();
326:         }
327: 
328:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
329: 
330:         uint256 totalUsdcTaken = 0;
331: 
332:         for (
333:             uint256 i;
334:             i < orderIdsToTake.length && totalUsdcTaken < amountUsdcToTakeInNativeDecimals;
335:         ) {
336:             uint256 orderId = orderIdsToTake[i];
337:             UsdcOrder storage order = $.orders[orderId];
338: 
339:             if (order.active) {
340:                 uint256 remainingAmountToTake = amountUsdcToTakeInNativeDecimals - totalUsdcTaken;
341:                 
342:                 uint256 amountOfUsdcFromOrder = order.tokenAmount > remainingAmountToTake
343:                     ? remainingAmountToTake
344:                     : order.tokenAmount;
345: 
346:                 
347:                 
348:                 
349: 
350:                 order.tokenAmount -= amountOfUsdcFromOrder;
351:                 totalUsdcTaken += amountOfUsdcFromOrder;
352: 
353:                 if (order.tokenAmount == 0) {
354:                     order.active = false;
355:                 }
356: 
357:                 uint256 usd0Amount = _getUsd0WadEquivalent(amountOfUsdcFromOrder, usdcWadPrice);
358:                 totalUsd0Provided += usd0Amount;
359:                 
360:                 $.usd0.safeTransferFrom(msg.sender, order.requester, usd0Amount); // <= FOUND
361: 
362:                 emit OrderMatched(order.requester, msg.sender, orderId, amountOfUsdcFromOrder);
363:             }
364: 
365:             unchecked {
366:                 ++i;
367:             }
368:         }
369: 
370:         
371:         $.usdcToken.safeTransfer(recipient, totalUsdcTaken); // <= FOUND
372:         
373:         if (
374:             !partialMatchingAllowed && totalUsdcTaken != amountUsdcToTakeInNativeDecimals
375:                 || totalUsdcTaken == 0
376:         ) {
377:             revert AmountTooLow();
378:         }
379: 
380:         return ((amountUsdcToTakeInNativeDecimals - totalUsdcTaken), totalUsd0Provided);
381:     }

['126']

126:     function transferFrom(address sender, address to, uint256 amount) // <= FOUND
127:         public
128:         override(ERC20Upgradeable, IERC20)
129:         returns (bool)
130:     {
131:         return super.transferFrom(sender, to, amount); // <= FOUND
132:     }

['289']

289:     function transferFrom(address sender, address recipient, uint256 amount) // <= FOUND
290:         public
291:         override(ERC20Upgradeable, IERC20)
292:         returns (bool)
293:     {
294:         return super.transferFrom(sender, recipient, amount); // <= FOUND
295:     }

[Gas-1] Using constants directly, rather than caching the value, saves gas

Resolution

In smart contract development, especially within Ethereum, gas optimization is crucial. Utilizing constants directly, instead of assigning them to a variable (caching) before use, can save gas as reading from a constant is cheaper than reading from storage or a variable. Constants in Solidity are replaced by their actual value in the EVM bytecode, eliminating the need for a SLOAD operation, which costs more gas. Hence, if a value will not change throughout the contract's life, defining it as a constant and using it directly in expressions/functions can be a gas-efficient practice.

Num of instances: 8

Findings

Click to show findings

['250']

250:         bytes32 position = DaoCollateralStorageV0Location; // <= FOUND

['39']

39:         bytes32 position = RegistryContractStorageV0Location; // <= FOUND

['105']

105:         bytes32 position = SwapperEngineStorageV0Location; // <= FOUND

['45']

45:         bytes32 position = TokenMappingStorageV0Location; // <= FOUND

['47']

47:         bytes32 position = Usd0StorageV0Location; // <= FOUND

['85']

85:         bytes32 position = Usd0PPStorageV0Location; // <= FOUND

['37']

37:         bytes32 position = UsualOracleStorageV0Location; // <= FOUND

['67']

67:         bytes32 position = AbstractOracleStorageV0Location; // <= FOUND

[Gas-2] Using named returns for pure and view functions is cheaper than using regular returns

Num of instances: 27

Findings

Click to show findings

['104']

104:     function getMaxDepegThreshold() external view returns (uint256) {
105:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
106:         return $.maxDepegThreshold; // <= FOUND
107:     }

['128']

128:     function getPrice(address token) public view override returns (uint256) {
129:         (uint256 price, uint256 decimalsPrice) = _latestRoundData(token);
130:         price = price.tokenAmountToWad(uint8(decimalsPrice));
131:         _checkDepegPrice(token, price);
132:         return price; // <= FOUND
133:     }

['136']

136:     function getQuote(address token, uint256 amount) external view override returns (uint256) {
137:         return Math.mulDiv(getPrice(token), amount, SCALAR_ONE); // <= FOUND
138:     }

['73']

73:     function _latestRoundData(address token) internal view override returns (uint256, uint256) {
74:         AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
75:         IAggregator priceAggregatorProxy = IAggregator($.tokenToOracleInfo[token].dataSource);
76: 
77:         if (address(priceAggregatorProxy) == address(0)) revert OracleNotInitialized();
78: 
79:         uint256 decimals = priceAggregatorProxy.decimals();
80: 
81:         
82:         (, int256 answer,, uint256 updatedAt,) = priceAggregatorProxy.latestRoundData();
83:         if (answer <= 0) revert OracleNotWorkingNotCurrent();
84:         if (updatedAt > block.timestamp) revert OracleNotWorkingNotCurrent();
85:         
86:         
87:         
88:         if (block.timestamp > $.tokenToOracleInfo[token].timeout + updatedAt) {
89:             revert OracleNotWorkingNotCurrent();
90:         }
91:         return (uint256(answer), decimals); // <= FOUND
92:     }

['811']

811:     function DOMAIN_SEPARATOR() external view virtual returns (bytes32) {
812:         return _domainSeparatorV4(); // <= FOUND
813:     }

['817']

817:     function isCBROn() external view returns (bool) {
818:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
819:         return $.isCBROn; // <= FOUND
820:     }

['823']

823:     function cbrCoef() public view returns (uint256) {
824:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
825:         return $.cbrCoef; // <= FOUND
826:     }

['829']

829:     function redeemFee() public view returns (uint256) {
830:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
831:         return $.redeemFee; // <= FOUND
832:     }

['835']

835:     function isRedeemPaused() public view returns (bool) {
836:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
837:         return $._redeemPaused; // <= FOUND
838:     }

['841']

841:     function isSwapPaused() public view returns (bool) {
842:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
843:         return $._swapPaused; // <= FOUND
844:     }

['106']

106:     function getContract(bytes32 name) external view returns (address) {
107:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
108:         address _contract = $._contracts[name];
109:         
110:         if (_contract == address(0)) {
111:             revert NullAddress();
112:         }
113: 
114:         return _contract; // <= FOUND
115:     }

['462']

462:     function getNextOrderId() external view override returns (uint256) {
463:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
464:         return $.nextOrderId; // <= FOUND
465:     }

['117']

117:     function getUsd0RwaById(uint256 rwaId) external view returns (address) {
118:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
119:         address rwa = $.USD0Rwas[rwaId];
120:         if (rwa == address(0)) {
121:             revert InvalidToken();
122:         }
123:         return rwa; // <= FOUND
124:     }

['127']

127:     function getAllUsd0Rwa() external view returns (address[] memory) {
128:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
129:         address[] memory rwas = new address[]($._usd0ToRwaLastId);
130:         
131:         uint256 length = $._usd0ToRwaLastId;
132:         for (uint256 i = 1; i <= length;) {
133:             rwas[i - 1] = $.USD0Rwas[i];
134:             unchecked {
135:                 ++i;
136:             }
137:         }
138:         return rwas; // <= FOUND
139:     }

['142']

142:     function getLastUsd0RwaId() external view returns (uint256) {
143:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
144:         return $._usd0ToRwaLastId; // <= FOUND
145:     }

['148']

148:     function isUsd0Collateral(address rwa) external view returns (bool) {
149:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
150:         return $.isUsd0Collateral[rwa]; // <= FOUND
151:     }

['210']

210:     function isBlacklisted(address account) external view returns (bool) {
211:         Usd0StorageV0 storage $ = _usd0StorageV0();
212:         return $.isBlacklisted[account]; // <= FOUND
213:     }

['252']

252:     function getStartTime() external view returns (uint256) {
253:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
254:         return $.bondStart; // <= FOUND
255:     }

['258']

258:     function getEndTime() external view returns (uint256) {
259:         Usd0PPStorageV0 storage $ = _usd0ppStorageV0();
260:         return $.bondStart + BOND_DURATION_FOUR_YEAR; // <= FOUND
261:     }

['90']

90:     function _latestRoundData(address token) internal view override returns (uint256, uint256) {
91:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
92:         IDataPublisher dataPublisher = IDataPublisher($.tokenToOracleInfo[token].dataSource);
93: 
94:         if (address(dataPublisher) == address(0)) revert OracleNotInitialized();
95: 
96:         
97:         (, int256 answer,, uint256 decimals) = dataPublisher.latestRoundData(token);
98: 
99:         if (answer <= 0) revert OracleNotWorkingNotCurrent();
100: 
101:         return (uint256(answer), decimals); // <= FOUND
102:     }

['48']

48:     function tokenAmountToWadWithTokenAddress(uint256 tokenAmount, address token)
49:         internal
50:         view
51:         returns (uint256, uint8)
52:     {
53:         uint8 tokenDecimals = uint8(IERC20Metadata(token).decimals());
54:         uint256 wadAmount = tokenAmountToWad(tokenAmount, uint8(tokenDecimals));
55:         return (wadAmount, tokenDecimals); // <= FOUND
56:     }

['247']

247:     function totalBondTimes() public pure returns (uint256) {
248:         return BOND_DURATION_FOUR_YEAR; // <= FOUND
249:     }

['16']

16:     function tokenAmountToDecimals(uint256 tokenAmount, uint8 tokenDecimals, uint8 targetDecimals)
17:         internal
18:         pure
19:         returns (uint256)
20:     {
21:         if (tokenDecimals < targetDecimals) {
22:             return tokenAmount * (10 ** uint256(targetDecimals - tokenDecimals)); // <= FOUND
23:         } else if (tokenDecimals > targetDecimals) {
24:             return tokenAmount / (10 ** uint256(tokenDecimals - targetDecimals)); // <= FOUND
25:         } else {
26:             return tokenAmount; // <= FOUND
27:         }
28:     }

['34']

34:     function tokenAmountToWad(uint256 tokenAmount, uint8 tokenDecimals)
35:         internal
36:         pure
37:         returns (uint256)
38:     {
39:         return tokenAmountToDecimals(tokenAmount, tokenDecimals, 18); // <= FOUND
40:     }

['64']

64:     function wadAmountByPrice(uint256 wadAmount, uint256 wadPrice)
65:         internal
66:         pure
67:         returns (uint256)
68:     {
69:         return Math.mulDiv(wadAmount, wadPrice, SCALAR_ONE, Math.Rounding.Floor); // <= FOUND
70:     }

['78']

78:     function wadTokenAmountForPrice(uint256 wadStableAmount, uint256 wadPrice, uint8 tokenDecimals)
79:         internal
80:         pure
81:         returns (uint256)
82:     {
83:         return Math.mulDiv(wadStableAmount, 10 ** tokenDecimals, wadPrice, Math.Rounding.Floor); // <= FOUND
84:     }

['90']

90:     function wadAmountToDecimals(uint256 wadAmount, uint8 targetDecimals)
91:         internal
92:         pure
93:         returns (uint256)
94:     {
95:         return tokenAmountToDecimals(wadAmount, 18, targetDecimals); // <= FOUND
96:     }

[Gas-3] Public functions not used internally can be marked as external to save gas

Resolution

Public functions that aren't used internally in Solidity contracts should be made external to optimize gas usage and improve contract efficiency. External functions can only be called from outside the contract, and their arguments are directly read from the calldata, which is more gas-efficient than loading them into memory, as is the case for public functions. By using external visibility, developers can reduce gas consumption for external calls and ensure that the contract operates more cost-effectively for users. Moreover, setting the appropriate visibility level for functions also enhances code readability and maintainability, promoting a more secure and well-structured contract design.

Num of instances: 17

Findings

Click to show findings

['124']

124:     function initialize(address registryContract) public initializer 

['260']

260:     function initialize(address _registryContract, uint256 _redeemFee) public initializer 

['27']

27:     function initialize(address deployer) public initializer 

['67']

67:     function initialize(address registryAccess_) public initializer 

['124']

124:     function initialize(address registryContract) public initializer 

['74']

74:     function initialize(address registryAccess, address registryContract) public initializer 

['67']

67:     function initialize(address registryContract_, string memory name_, string memory symbol_)
68:         public
69:         initializer
70:     

['108']

108:     function initialize(
109:         address registryContract,
110:         string memory name_,
111:         string memory symbol_,
112:         uint256 startTime
113:     ) public initializer 

['56']

56:     function initialize(address registryContractAddress) public initializer 

['293']

293:     function initializeV1(address _registryContract) public reinitializer(2) 

['823']

823:     function cbrCoef() public view returns (uint256) 

['829']

829:     function redeemFee() public view returns (uint256) 

['835']

835:     function isRedeemPaused() public view returns (bool) 

['841']

841:     function isSwapPaused() public view returns (bool) 

['149']

149:     function minimumUSDCAmountProvided() public view returns (uint256 minimumUSDCAmount) 

['167']

167:     function getOrder(uint256 orderId) public view returns (bool active, uint256 tokenAmount) 

['247']

247:     function totalBondTimes() public pure returns (uint256) 

[Gas-4] Calldata should be used in place of memory function parameters when not mutated

Resolution

In Solidity, calldata should be used in place of memory for function parameters when the function is external. This is because calldata is a non-modifiable, non-persistent area where function arguments are stored, and it's cheaper in terms of gas than memory. It's especially efficient for arrays and complex data types. calldata provides a gas-efficient way to pass data in external function calls, whereas memory is a temporary space that's erased between external function calls. This distinction is crucial for optimizing smart contracts for gas usage and performance.

Num of instances: 3

Findings

Click to show findings

['384']

384:     function provideUsd0ReceiveUSDC(
385:         address recipient,
386:         uint256 amountUsdcToTakeInNativeDecimals,
387:         uint256[] memory orderIdsToTake, // <= FOUND
388:         bool partialMatchingAllowed
389:     ) external nonReentrant whenNotPaused returns (uint256) {
390:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
391:         uint256 usdcWadPrice = _getUsdcWadPrice();
392:         uint256 requiredUsd0Amount =
393:             _getUsd0WadEquivalent(amountUsdcToTakeInNativeDecimals, usdcWadPrice);
394:         if ($.usd0.balanceOf(msg.sender) < requiredUsd0Amount) {
395:             revert InsufficientUSD0Balance();
396:         }
397:         (uint256 unmatchedUsdcAmount,) = _provideUsd0ReceiveUSDC(
398:             recipient,
399:             amountUsdcToTakeInNativeDecimals,
400:             orderIdsToTake,
401:             partialMatchingAllowed,
402:             usdcWadPrice
403:         );
404:         return unmatchedUsdcAmount;
405:     }

['408']

408:     function provideUsd0ReceiveUSDCWithPermit(
409:         address recipient,
410:         uint256 amountUsdcToTakeInNativeDecimals,
411:         uint256[] memory orderIdsToTake, // <= FOUND
412:         bool partialMatchingAllowed,
413:         uint256 usd0ToPermit,
414:         uint256 deadline,
415:         uint8 v,
416:         bytes32 r,
417:         bytes32 s
418:     ) external nonReentrant whenNotPaused returns (uint256) {
419:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
420:         uint256 usdcWadPrice = _getUsdcWadPrice();
421:         uint256 requiredUsd0Amount =
422:             _getUsd0WadEquivalent(amountUsdcToTakeInNativeDecimals, usdcWadPrice);
423:         
424:         if ($.usd0.balanceOf(msg.sender) < requiredUsd0Amount || usd0ToPermit < requiredUsd0Amount)
425:         {
426:             revert InsufficientUSD0Balance();
427:         }
428:         try IERC20Permit(address($.usd0)).permit(
429:             msg.sender, address(this), usd0ToPermit, deadline, v, r, s
430:         ) {} catch {} 
431:         (uint256 unmatchedUsdcAmount,) = _provideUsd0ReceiveUSDC(
432:             recipient,
433:             amountUsdcToTakeInNativeDecimals,
434:             orderIdsToTake,
435:             partialMatchingAllowed,
436:             usdcWadPrice
437:         );
438:         return unmatchedUsdcAmount;
439:     }

['442']

442:     function swapUsd0(
443:         address recipient,
444:         uint256 amountUsd0ToProvideInWad,
445:         uint256[] memory orderIdsToTake, // <= FOUND
446:         bool partialMatchingAllowed
447:     ) external nonReentrant whenNotPaused returns (uint256) {
448:         uint256 usdcWadPrice = _getUsdcWadPrice();
449: 
450:         (, uint256 totalUsd0Provided) = _provideUsd0ReceiveUSDC(
451:             recipient,
452:             _getUsdcAmountFromUsd0WadEquivalent(amountUsd0ToProvideInWad, usdcWadPrice),
453:             orderIdsToTake,
454:             partialMatchingAllowed,
455:             usdcWadPrice
456:         );
457: 
458:         return amountUsd0ToProvideInWad - totalUsd0Provided;
459:     }

[Gas-5] Usage of smaller uint/int types causes overhead

Resolution

When using a smaller int/uint type it first needs to be converted to it's 258 bit counterpart to be operated, this increases the gass cost and thus should be avoided. However it does make sense to use smaller int/uint values within structs provided you pack the struct properly.

Num of instances: 26

Findings

Click to show findings

['459']

459:     
463:     function _getPriceAndDecimals(address rwaToken)
464:         internal
465:         view
466:         returns (uint256 wadPriceInUSD, uint8 decimals) // <= FOUND
467:     {

['479']

479:         (uint256 wadPriceInUSD, uint8 decimals) = _getPriceAndDecimals(rwaToken); // <= FOUND

['531']

531:         uint8 tokenDecimals = IERC20Metadata(rwaToken).decimals(); // <= FOUND

['685']

685:     
686:     function swapWithPermit(
687:         address rwaToken,
688:         uint256 amount,
689:         uint256 minAmountOut,
690:         uint256 deadline,
691:         uint8 v, // <= FOUND
692:         bytes32 r,
693:         bytes32 s
694:     ) external {

['7']

7:     uint8 v;  // <= FOUND

['38']

38:     
47:     function swapWithPermit(
48:         address rwaToken,
49:         uint256 amount,
50:         uint256 minAmountOut,
51:         uint256 deadline,
52:         uint8 v, // <= FOUND
53:         bytes32 r,
54:         bytes32 s
55:     ) external;

['11']

11:     
16:     function latestRoundData(address token)
17:         external
18:         view
19:         returns (uint80 roundId, int256 answer, uint256 timestamp, uint8 decimals); // <= FOUND

['22']

22:     
28:     function getRoundData(address token, uint80 roundId) // <= FOUND
29:         external
30:         view
31:         returns (uint80 id, int256 answer, uint256 timestamp, uint8 decimals); // <= FOUND

['20']

20:     
28:     function depositUSDCWithPermit(
29:         uint256 amountToDeposit,
30:         uint256 deadline,
31:         uint8 v, // <= FOUND
32:         bytes32 r,
33:         bytes32 s
34:     ) external;

['61']

61:     
73:     function provideUsd0ReceiveUSDCWithPermit(
74:         address recipient,
75:         uint256 amountUsdcToTakeInNativeDecimals,
76:         uint256[] memory orderIdsToTake,
77:         bool partialMatchingAllowed,
78:         uint256 usd0ToPermit,
79:         uint256 deadline,
80:         uint8 v, // <= FOUND
81:         bytes32 r,
82:         bytes32 s
83:     ) external returns (uint256);

['34']

34:     
41:     function mintWithPermit(uint256 amountUsd0, uint256 deadline, uint8 v, bytes32 r, bytes32 s) // <= FOUND
42:         external;

['186']

186:         uint8 decimals = IERC20Metadata(address($.usdcToken)).decimals(); // <= FOUND

['266']

266:     
267:     function depositUSDCWithPermit(
268:         uint256 amountToDeposit,
269:         uint256 deadline,
270:         uint8 v, // <= FOUND
271:         bytes32 r,
272:         bytes32 s
273:     ) external nonReentrant whenNotPaused {

['408']

408:     
409:     function provideUsd0ReceiveUSDCWithPermit(
410:         address recipient,
411:         uint256 amountUsdcToTakeInNativeDecimals,
412:         uint256[] memory orderIdsToTake,
413:         bool partialMatchingAllowed,
414:         uint256 usd0ToPermit,
415:         uint256 deadline,
416:         uint8 v, // <= FOUND
417:         bytes32 r,
418:         bytes32 s
419:     ) external nonReentrant whenNotPaused returns (uint256) {

['189']

189:     
190:     function mintWithPermit(uint256 amountUsd0, uint256 deadline, uint8 v, bytes32 r, bytes32 s) // <= FOUND
191:         external
192:     {

['16']

16:     
21:     function tokenAmountToDecimals(uint256 tokenAmount, uint8 tokenDecimals, uint8 targetDecimals) // <= FOUND
22:         internal
23:         pure
24:         returns (uint256)
25:     {

['34']

34:     
38:     function tokenAmountToWad(uint256 tokenAmount, uint8 tokenDecimals) // <= FOUND
39:         internal
40:         pure
41:         returns (uint256)
42:     {

['53']

53:         uint8 tokenDecimals = uint8(IERC20Metadata(token).decimals()); // <= FOUND

['78']

78:     
84:     function wadTokenAmountForPrice(uint256 wadStableAmount, uint256 wadPrice, uint8 tokenDecimals) // <= FOUND
85:         internal
86:         pure
87:         returns (uint256)
88:     {

['90']

90:     
94:     function wadAmountToDecimals(uint256 wadAmount, uint8 targetDecimals) // <= FOUND
95:         internal
96:         pure
97:         returns (uint256)
98:     {

['35']

35:         
36:         uint64 timeout; // <= FOUND

['47']

47:     
52:     function initializeTokenOracle(
53:         address token,
54:         address dataSource,
55:         uint64 timeout, // <= FOUND
56:         bool isStablecoin
57:     ) external {

['69']

69:     
74:     function initializeTokenOracle(address token, uint64 timeout, bool isStablecoin) external { // <= FOUND

['39']

39: uint64 constant ONE_WEEK = 604_800; // <= FOUND

['12']

12:     function getRoundData(uint80 _roundId) // <= FOUND
13:         external
14:         view
15:         returns (
16:             uint80 roundId, // <= FOUND
17:             int256 answer,
18:             uint256 startedAt,
19:             uint256 updatedAt,
20:             uint80 answeredInRound // <= FOUND
21:         );

['23']

23:     function latestRoundData()
24:         external
25:         view
26:         returns (
27:             uint80 roundId, // <= FOUND
28:             int256 answer,
29:             uint256 startedAt,
30:             uint256 updatedAt,
31:             uint80 answeredInRound // <= FOUND
32:         );

[Gas-6] Use != 0 instead of > 0

Resolution

Replace spotted instances with != 0 for uints as this uses less gas

Num of instances: 2

Findings

Click to show findings

['541']

541:         
542:         if (stableFee > 0) { // <= FOUND

['637']

637:         
638:         if (wadRwaNotTakenInUSD > 0) { // <= FOUND

[Gas-7] Integer increments by one can be unchecked to save on gas fees

Resolution

Using unchecked increments in Solidity can save on gas fees by bypassing built-in overflow checks, thus optimizing gas usage, but requires careful assessment of potential risks and edge cases to avoid unintended consequences.

Num of instances: 2

Findings

Click to show findings

['236']

236:     function _depositUSDC(SwapperEngineStorageV0 storage $, uint256 amountToDeposit) internal { // <= FOUND
237:         if (amountToDeposit < $.minimumUSDCAmountProvided) {
238:             
239:             revert AmountTooLow();
240:         }
241:         if (IUsd0(address($.usd0)).isBlacklisted(msg.sender)) {
242:             revert NotAuthorized();
243:         }
244: 
245:         uint256 orderId = $.nextOrderId++; // <= FOUND
246:         $.orders[orderId] =
247:             UsdcOrder({requester: msg.sender, tokenAmount: amountToDeposit, active: true});
248: 
249:         
250:         $.usdcToken.safeTransferFrom(msg.sender, address(this), amountToDeposit);
251: 
252:         emit Deposit(msg.sender, orderId, amountToDeposit);
253:     }

['85']

85:     function addUsd0Rwa(address rwa) external returns (bool) { // <= FOUND
86:         if (rwa == address(0)) {
87:             revert NullAddress();
88:         }
89:         
90:         
91:         
92:         if (IERC20Metadata(rwa).decimals() == 0) {
93:             revert Invalid();
94:         }
95: 
96:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
97:         $._registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
98: 
99:         
100:         if ($.isUsd0Collateral[rwa]) revert SameValue();
101:         $.isUsd0Collateral[rwa] = true;
102:         
103:         ++$._usd0ToRwaLastId; // <= FOUND
104:         if ($._usd0ToRwaLastId > MAX_RWA_COUNT) {
105:             revert TooManyRWA();
106:         }
107:         $.USD0Rwas[$._usd0ToRwaLastId] = rwa;
108:         emit AddUsd0Rwa(rwa, $._usd0ToRwaLastId);
109:         return true;
110:     }

[Gas-8] Default bool values are manually reset

Resolution

Using .delete is better than resetting a Solidity variable to its default value manually because it frees up storage space on the Ethereum blockchain, resulting in gas cost savings.

Num of instances: 6

Findings

Click to show findings

['333']

333:     function deactivateCBR() external { // <= FOUND
334:         _requireOnlyAdmin();
335:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
336:         if ($.isCBROn == false) revert SameValue();
337:         $.isCBROn = false; // <= FOUND
338:         emit CBRDeactivated();
339:     }

['364']

364:     function unpauseRedeem() external whenRedeemPaused { // <= FOUND
365:         _requireOnlyAdmin();
366:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
367:         $._redeemPaused = false; // <= FOUND
368:         emit RedeemUnPaused();
369:     }

['382']

382:     function unpauseSwap() external whenSwapPaused { // <= FOUND
383:         _requireOnlyAdmin();
384:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
385:         $._swapPaused = false; // <= FOUND
386:         emit SwapUnPaused();
387:     }

['281']

281:     function withdrawUSDC(uint256 orderToCancel) external nonReentrant whenNotPaused { // <= FOUND
282:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
283:         UsdcOrder storage order = $.orders[orderToCancel];
284: 
285:         if (!order.active) {
286:             
287:             revert OrderNotActive();
288:         }
289:         if (order.requester != msg.sender) {
290:             
291:             revert NotRequester();
292:         }
293: 
294:         uint256 amountToWithdraw = order.tokenAmount;
295:         order.active = false;  // <= FOUND
296:         order.tokenAmount = 0; 
297: 
298:         
299:         $.usdcToken.safeTransfer(msg.sender, amountToWithdraw);
300: 
301:         emit Withdraw(msg.sender, orderToCancel, amountToWithdraw);
302:     }

['313']

313:     function _provideUsd0ReceiveUSDC( 
314:         address recipient,
315:         uint256 amountUsdcToTakeInNativeDecimals,
316:         uint256[] memory orderIdsToTake,
317:         bool partialMatchingAllowed,
318:         uint256 usdcWadPrice
319:     ) internal returns (uint256 unmatchedUsdcAmount, uint256 totalUsd0Provided) {
320:         if (amountUsdcToTakeInNativeDecimals == 0) {
321:             
322:             revert AmountIsZero();
323:         }
324:         if (orderIdsToTake.length == 0) {
325:             revert NoOrdersIdsProvided();
326:         }
327: 
328:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
329: 
330:         uint256 totalUsdcTaken = 0;
331: 
332:         for (
333:             uint256 i;
334:             i < orderIdsToTake.length && totalUsdcTaken < amountUsdcToTakeInNativeDecimals;
335:         ) {
336:             uint256 orderId = orderIdsToTake[i];
337:             UsdcOrder storage order = $.orders[orderId];
338: 
339:             if (order.active) {
340:                 uint256 remainingAmountToTake = amountUsdcToTakeInNativeDecimals - totalUsdcTaken;
341:                 
342:                 uint256 amountOfUsdcFromOrder = order.tokenAmount > remainingAmountToTake
343:                     ? remainingAmountToTake
344:                     : order.tokenAmount;
345: 
346:                 
347:                 
348:                 
349: 
350:                 order.tokenAmount -= amountOfUsdcFromOrder;
351:                 totalUsdcTaken += amountOfUsdcFromOrder;
352: 
353:                 if (order.tokenAmount == 0) {
354:                     order.active = false; // <= FOUND
355:                 }
356: 
357:                 uint256 usd0Amount = _getUsd0WadEquivalent(amountOfUsdcFromOrder, usdcWadPrice);
358:                 totalUsd0Provided += usd0Amount;
359:                 
360:                 $.usd0.safeTransferFrom(msg.sender, order.requester, usd0Amount);
361: 
362:                 emit OrderMatched(order.requester, msg.sender, orderId, amountOfUsdcFromOrder);
363:             }
364: 
365:             unchecked {
366:                 ++i;
367:             }
368:         }
369: 
370:         
371:         $.usdcToken.safeTransfer(recipient, totalUsdcTaken);
372:         
373:         if (
374:             !partialMatchingAllowed && totalUsdcTaken != amountUsdcToTakeInNativeDecimals
375:                 || totalUsdcTaken == 0
376:         ) {
377:             revert AmountTooLow();
378:         }
379: 
380:         return ((amountUsdcToTakeInNativeDecimals - totalUsdcTaken), totalUsd0Provided);
381:     }

['198']

198:     function unBlacklist(address account) external { // <= FOUND
199:         Usd0StorageV0 storage $ = _usd0StorageV0();
200:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
201:         if (!$.isBlacklisted[account]) {
202:             revert SameValue();
203:         }
204:         $.isBlacklisted[account] = false; // <= FOUND
205: 
206:         emit UnBlacklist(account);
207:     }

[Gas-9] Default int values are manually reset

Resolution

Using .delete is better than resetting a Solidity variable to its default value manually because it frees up storage space on the Ethereum blockchain, resulting in gas cost savings.

Num of instances: 1

Findings

Click to show findings

['296']

296:         order.tokenAmount = 0;  // <= FOUND

[Gas-10] Use assembly to check for the zero address

Resolution

Using assembly for address comparisons in Solidity can save gas because it allows for more direct access to the Ethereum Virtual Machine (EVM), reducing the overhead of higher-level operations. Solidity's high-level abstraction simplifies coding but can introduce additional gas costs. Using assembly for simple operations like address comparisons can be more gas-efficient.

Num of instances: 17

Findings

Click to show findings

['87']

87:     function __AbstractOracle_init_unchained(address registryContract) internal onlyInitializing {
88:         if (registryContract == address(0)) { // <= FOUND
89:             revert NullAddress();
90:         }
91: 
92:         AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
93:         $.registryContract = IRegistryContract(registryContract);
94:         $.registryAccess = IRegistryAccess($.registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
95:         $.maxDepegThreshold = INITIAL_MAX_DEPEG_THRESHOLD;
96:         emit SetMaxDepegThreshold(INITIAL_MAX_DEPEG_THRESHOLD);
97:     }

['47']

47:     function initializeTokenOracle(
48:         address token,
49:         address dataSource,
50:         uint64 timeout,
51:         bool isStablecoin
52:     ) external {
53:         if (token == address(0)) revert NullAddress(); // <= FOUND
54:         if (dataSource == address(0)) revert NullAddress(); // <= FOUND
55:         
56:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout();
57: 
58:         
59:         (, int256 answer,, uint256 updatedAt,) = IAggregator(dataSource).latestRoundData();
60:         if (answer <= 0 || updatedAt == 0 || block.timestamp > updatedAt + timeout) {
61:             revert OracleNotWorkingNotCurrent();
62:         }
63: 
64:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
65:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
66: 
67:         $.tokenToOracleInfo[token].dataSource = dataSource;
68:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
69:         $.tokenToOracleInfo[token].timeout = timeout;
70:     }

['73']

73:     function _latestRoundData(address token) internal view override returns (uint256, uint256) {
74:         AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
75:         IAggregator priceAggregatorProxy = IAggregator($.tokenToOracleInfo[token].dataSource);
76: 
77:         if (address(priceAggregatorProxy) == address(0)) revert OracleNotInitialized(); // <= FOUND
78: 
79:         uint256 decimals = priceAggregatorProxy.decimals();
80: 
81:         
82:         (, int256 answer,, uint256 updatedAt,) = priceAggregatorProxy.latestRoundData();
83:         if (answer <= 0) revert OracleNotWorkingNotCurrent();
84:         if (updatedAt > block.timestamp) revert OracleNotWorkingNotCurrent();
85:         
86:         
87:         
88:         if (block.timestamp > $.tokenToOracleInfo[token].timeout + updatedAt) {
89:             revert OracleNotWorkingNotCurrent();
90:         }
91:         return (uint256(answer), decimals);
92:     }

['260']

260:     function initialize(address _registryContract, uint256 _redeemFee) public initializer {
261:         
262:         if (_redeemFee > MAX_REDEEM_FEE) {
263:             revert RedeemFeeTooBig();
264:         }
265: 
266:         if (_registryContract == address(0)) { // <= FOUND
267:             revert NullContract();
268:         }
269: 
270:         __EIP712_init_unchained("DaoCollateral", "1");
271:         __Nonces_init_unchained();
272:         __Pausable_init_unchained();
273:         __ReentrancyGuard_init_unchained();
274: 
275:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
276:         $.redeemFee = _redeemFee;
277: 
278:         IRegistryContract registryContract = IRegistryContract(_registryContract);
279:         $.registryAccess = IRegistryAccess(registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
280: 
281:         $.treasury = address(registryContract.getContract(CONTRACT_TREASURY));
282:         $.tokenMapping = ITokenMapping(registryContract.getContract(CONTRACT_TOKEN_MAPPING));
283:         $.usd0 = IUsd0(registryContract.getContract(CONTRACT_USD0));
284: 
285:         $.oracle = IOracle(registryContract.getContract(CONTRACT_ORACLE));
286: 
287:         $.swapperEngine = ISwapperEngine(registryContract.getContract(CONTRACT_SWAPPER_ENGINE));
288:     }

['293']

293:     function initializeV1(address _registryContract) public reinitializer(2) {
294:         if (_registryContract == address(0)) { // <= FOUND
295:             revert NullContract();
296:         }
297: 
298:         __EIP712_init_unchained("DaoCollateral", "1");
299:         __Nonces_init_unchained();
300: 
301:         DaoCollateralStorageV0 storage $ = _daoCollateralStorageV0();
302:         $.registryContract = IRegistryContract(_registryContract);
303:         $.swapperEngine = ISwapperEngine(
304:             IRegistryContract(_registryContract).getContract(CONTRACT_SWAPPER_ENGINE)
305:         );
306:     }

['27']

27:     function initialize(address deployer) public initializer {
28:         
29:         if (deployer == address(0)) { // <= FOUND
30:             revert NullAddress();
31:         }
32:         __AccessControl_init_unchained();
33:         __AccessControlDefaultAdminRules_init_unchained(
34:             3 days,
35:             deployer 
36:         );
37:     }

['67']

67:     function initialize(address registryAccess_) public initializer {
68:         if (registryAccess_ == address(0)) { // <= FOUND
69:             revert NullAddress();
70:         }
71: 
72:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
73:         $._registryAccess = registryAccess_;
74:     }

['83']

83:     function setContract(bytes32 name, address contractAddress) external {
84:         
85:         if (contractAddress == address(0)) { // <= FOUND
86:             revert NullAddress();
87:         }
88:         
89:         if (name == bytes32(0)) {
90:             revert InvalidName();
91:         }
92: 
93:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
94:         
95:         if (!IRegistryAccess($._registryAccess).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
96:             revert NotAuthorized();
97:         }
98: 
99:         $._contracts[name] = contractAddress;
100:         emit SetContract(name, contractAddress);
101:     }

['106']

106:     function getContract(bytes32 name) external view returns (address) {
107:         RegistryContractStorageV0 storage $ = _registryContractStorageV0();
108:         address _contract = $._contracts[name];
109:         
110:         if (_contract == address(0)) { // <= FOUND
111:             revert NullAddress();
112:         }
113: 
114:         return _contract;
115:     }

['124']

124:     function initialize(address registryContract) public initializer {
125:         if (registryContract == address(0)) { // <= FOUND
126:             revert NullContract();
127:         }
128:         __Pausable_init_unchained();
129:         __ReentrancyGuard_init_unchained();
130:         SwapperEngineStorageV0 storage $ = _swapperEngineStorageV0();
131:         $.registryContract = IRegistryContract(registryContract);
132:         $.registryAccess = IRegistryAccess($.registryContract.getContract(CONTRACT_REGISTRY_ACCESS));
133:         $.usdcToken = IERC20($.registryContract.getContract(CONTRACT_USDC));
134:         $.usd0 = IERC20($.registryContract.getContract(CONTRACT_USD0));
135:         $.nextOrderId = 1;
136:         $.oracle = IOracle($.registryContract.getContract(CONTRACT_ORACLE));
137:         $.minimumUSDCAmountProvided = MINIMUM_USDC_PROVIDED;
138:     }

['74']

74:     function initialize(address registryAccess, address registryContract) public initializer {
75:         if (registryAccess == address(0) || registryContract == address(0)) { // <= FOUND
76:             revert NullAddress();
77:         }
78: 
79:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
80:         $._registryAccess = IRegistryAccess(registryAccess);
81:         $._registryContract = IRegistryContract(registryContract);
82:     }

['85']

85:     function addUsd0Rwa(address rwa) external returns (bool) {
86:         if (rwa == address(0)) { // <= FOUND
87:             revert NullAddress();
88:         }
89:         
90:         
91:         
92:         if (IERC20Metadata(rwa).decimals() == 0) {
93:             revert Invalid();
94:         }
95: 
96:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
97:         $._registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
98: 
99:         
100:         if ($.isUsd0Collateral[rwa]) revert SameValue();
101:         $.isUsd0Collateral[rwa] = true;
102:         
103:         ++$._usd0ToRwaLastId;
104:         if ($._usd0ToRwaLastId > MAX_RWA_COUNT) {
105:             revert TooManyRWA();
106:         }
107:         $.USD0Rwas[$._usd0ToRwaLastId] = rwa;
108:         emit AddUsd0Rwa(rwa, $._usd0ToRwaLastId);
109:         return true;
110:     }

['117']

117:     function getUsd0RwaById(uint256 rwaId) external view returns (address) {
118:         TokenMappingStorageV0 storage $ = _tokenMappingStorageV0();
119:         address rwa = $.USD0Rwas[rwaId];
120:         if (rwa == address(0)) { // <= FOUND
121:             revert InvalidToken();
122:         }
123:         return rwa;
124:     }

['67']

67:     function initialize(address registryContract_, string memory name_, string memory symbol_)
68:         public
69:         initializer
70:     {
71:         
72:         __ERC20_init_unchained(name_, symbol_);
73:         
74:         __Pausable_init_unchained();
75:         
76:         __ERC20Permit_init_unchained(name_);
77:         
78:         __EIP712_init_unchained(name_, "1");
79:         
80:         if (registryContract_ == address(0)) { // <= FOUND
81:             revert NullContract();
82:         }
83:         _usd0StorageV0().registryAccess = IRegistryAccess(
84:             IRegistryContract(registryContract_).getContract(CONTRACT_REGISTRY_ACCESS)
85:         );
86:     }

['181']

181:     function blacklist(address account) external {
182:         if (account == address(0)) { // <= FOUND
183:             revert NullAddress();
184:         }
185:         Usd0StorageV0 storage $ = _usd0StorageV0();
186:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
187:         if ($.isBlacklisted[account]) {
188:             revert SameValue();
189:         }
190:         $.isBlacklisted[account] = true;
191: 
192:         emit Blacklist(account);
193:     }

['69']

69:     function initializeTokenOracle(address token, uint64 timeout, bool isStablecoin) external {
70:         if (token == address(0)) revert NullAddress(); // <= FOUND
71:         
72:         if (timeout == 0 || timeout > ONE_WEEK) revert InvalidTimeout();
73: 
74:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
75:         $.registryAccess.onlyMatchingRole(DEFAULT_ADMIN_ROLE);
76: 
77:         UsualOracleStorageV0 storage u = _usualOracleStorageV0();
78: 
79:         
80:         (, int256 answer, uint256 timestamp,) = u.dataPublisher.latestRoundData(token);
81:         if (answer <= 0 || block.timestamp - timestamp > timeout) {
82:             revert OracleNotWorkingNotCurrent();
83:         }
84: 
85:         $.tokenToOracleInfo[token].dataSource = address(u.dataPublisher);
86:         $.tokenToOracleInfo[token].isStablecoin = isStablecoin;
87:     }

['90']

90:     function _latestRoundData(address token) internal view override returns (uint256, uint256) {
91:         AbstractOracle.AbstractOracleStorageV0 storage $ = _abstractOracleStorageV0();
92:         IDataPublisher dataPublisher = IDataPublisher($.tokenToOracleInfo[token].dataSource);
93: 
94:         if (address(dataPublisher) == address(0)) revert OracleNotInitialized(); // <= FOUND
95: 
96:         
97:         (, int256 answer,, uint256 decimals) = dataPublisher.latestRoundData(token);
98: 
99:         if (answer <= 0) revert OracleNotWorkingNotCurrent();
100: 
101:         return (uint256(answer), decimals);
102:     }

[Gas-11] Can transfer 0

Resolution

In Solidity, performing unnecessary operations can consume more gas than needed, leading to cost inefficiencies. For instance, if a transfer function doesn't have a zero amount check and someone calls it with a zero amount, unnecessary gas will be consumed in executing the function, even though the state of the contract remains the same. By implementing a zero amount check, such unnecessary function calls can be avoided, thereby saving gas and making the contract more efficient.

Num of instances: 2

Findings

Click to show findings

['112']

112:     function transfer(address to, uint256 amount)
113:         public
114:         override(ERC20Upgradeable, IERC20)
115:         returns (bool)
116:     {
117:         return super.transfer(to, amount); // <= FOUND
118:     }

['221']

221:     function transfer(address recipient, uint256 amount)
222:         public
223:         override(ERC20Upgradeable, IERC20)
224:         returns (bool)
225:     {
226:         return super.transfer(recipient, amount); // <= FOUND
227:     }

[Gas-12] Structs can be packed into fewer storage slots

Resolution

In Solidity, each storage slot has a size of 32 bytes. If a struct contains multiple uint values, it's efficient to pack these into as few storage slots as possible to optimize gas usage. The EVM (Ethereum Virtual Machine) charges gas for each storage operation, so minimizing the number of slots used can result in substantial gas savings. This can be achieved by ordering struct fields according to their size or by using smaller data types where possible. However, developers must balance these optimizations with the need for code clarity and the precision requirements of their application. Always ensure that data packing does not compromise the functionality or security of the contract.

Num of instances: 4

Findings

Click to show findings

['12']

12: struct Intent {
13:     address recipient;
14:     address rwaToken;
15:     uint256 amountInTokenDecimals; // <= FOUND
16:     uint256 deadline; // <= FOUND
17:     bytes signature;
18: }

['83']

83:     struct DaoCollateralStorageV0 {
84:         
85:         bool _redeemPaused;
86:         
87:         bool _swapPaused;
88:         
89:         bool isCBROn;
90:         
91:         uint256 redeemFee; // <= FOUND
92:         
93:         uint256 cbrCoef; // <= FOUND
94:         
95:         IRegistryAccess registryAccess;
96:         
97:         IRegistryContract registryContract;
98:         
99:         ITokenMapping tokenMapping;
100:         
101:         IUsd0 usd0;
102:         
103:         IOracle oracle;
104:         
105:         address treasury;
106:         
107:         ISwapperEngine swapperEngine;
108:     }

['83']

83:     struct DaoCollateralStorageV0 {
84:         
85:         bool _redeemPaused;
86:         
87:         bool _swapPaused;
88:         
89:         bool isCBROn;
90:         
91:         uint256 redeemFee; // <= FOUND
92:         
93:         uint256 cbrCoef; // <= FOUND
94:         
95:         IRegistryAccess registryAccess;
96:         
97:         IRegistryContract registryContract;
98:         
99:         ITokenMapping tokenMapping;
100:         
101:         IUsd0 usd0;
102:         
103:         IOracle oracle;
104:         
105:         address treasury;
106:         
107:         ISwapperEngine swapperEngine;
108:     }

['9']

9:     struct Reward {
10:         address token;
11:         address distributor;
12:         uint256 period_finish; // <= FOUND
13:         uint256 rate; // <= FOUND
14:         uint256 last_update; // <= FOUND
15:         uint256 integral; // <= FOUND
16:     }

[Gas-13] Private functions used once can be inlined

Resolution

Private functions which are only called once can be inlined to save GAS.

Num of instances: 1

Findings

Click to show findings

['196']

196:     function _getUsdcAmountFromUsd0WadEquivalent(uint256 usd0WadAmount, uint256 usdcWadPrice) // <= FOUND
197:         private
198:         view
199:         returns (uint256 usdcTokenAmountInNativeDecimals)
200:     

[Gas-14] Use assembly to emit events

Resolution

With the use of inline assembly in Solidity, we can take advantage of low-level features like scratch space and the free memory pointer, offering more gas-efficient ways of emitting events. The scratch space is a certain area of memory where we can temporarily store data, and the free memory pointer indicates the next available memory slot. Using these, we can efficiently assemble event data without incurring additional memory expansion costs. However, safety is paramount: to avoid overwriting or leakage, we must cache the free memory pointer before use and restore it afterward, ensuring that it points to the correct memory location post-operation.

Num of instances: 24

Findings

Click to show findings

['96']

96:         emit SetMaxDepegThreshold(INITIAL_MAX_DEPEG_THRESHOLD); // <= FOUND

['122']

122:         emit SetMaxDepegThreshold(maxAuthorizedDepegPrice); // <= FOUND

['327']

327:         emit CBRActivated($.cbrCoef); // <= FOUND

['328']

328:         emit SwapPaused(); // <= FOUND

['338']

338:         emit CBRDeactivated(); // <= FOUND

['350']

350:         emit RedeemFeeUpdated(_redeemFee); // <= FOUND

['359']

359:         emit RedeemPaused(); // <= FOUND

['368']

368:         emit RedeemUnPaused(); // <= FOUND

['386']

386:         emit SwapUnPaused(); // <= FOUND

['658']

658:         emit Swap(caller, rwaToken, matchedAmountInTokenDecimals, matchedAmountInUSD); // <= FOUND

['681']

681:         
682:         emit Swap(msg.sender, rwaToken, amount, wadQuoteInUSD); // <= FOUND

['723']

723:         emit Redeem(msg.sender, rwaToken, amount, returnedCollateral, stableFee); // <= FOUND

['739']

739:         emit Redeem(msg.sender, rwaToken, amount, returnedCollateral, 0); // <= FOUND

['758']

758:         emit NonceInvalidated(msg.sender, nonceUsed); // <= FOUND

['805']

805:         emit IntentMatched( // <= FOUND
806:             intent.recipient, nonce, intent.rwaToken, amountInTokenDecimals, amountInUSD
807:         );

['100']

100:         emit SetContract(name, contractAddress); // <= FOUND

['252']

252:         emit Deposit(msg.sender, orderId, amountToDeposit); // <= FOUND

['301']

301:         emit Withdraw(msg.sender, orderToCancel, amountToWithdraw); // <= FOUND

['362']

362:                 emit OrderMatched(order.requester, msg.sender, orderId, amountOfUsdcFromOrder); // <= FOUND

['108']

108:         emit AddUsd0Rwa(rwa, $._usd0ToRwaLastId); // <= FOUND

['192']

192:         emit Blacklist(account); // <= FOUND

['206']

206:         emit UnBlacklist(account); // <= FOUND

['243']

243:         emit BondUnwrapped(msg.sender, usd0PPBalance); // <= FOUND

['281']

281:         emit EmergencyWithdraw(safeAccount, balance); // <= FOUND

[Gas-15] Use solady library where possible to save gas

Resolution

The following OpenZeppelin imports have a Solady equivalent, as such they can be used to save GAS as Solady modules have been specifically designed to be as GAS efficient as possible

Num of instances: 7

Findings

Click to show findings

['8']

8: import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; // <= FOUND

['18']

18: import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; // <= FOUND

['19']

19: import {IERC20Permit} from "openzeppelin-contracts/token/ERC20/extensions/IERC20Permit.sol"; // <= FOUND

['5']

5: import {IERC20Metadata} from "openzeppelin-contracts/token/ERC20/extensions/IERC20Metadata.sol"; // <= FOUND

['8']

8: import {IERC20Metadata} from "openzeppelin-contracts/interfaces/IERC20Metadata.sol"; // <= FOUND

['9']

9: import {ERC20Upgradeable} from "openzeppelin-contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; // <= FOUND

['7']

7: import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol"; // <= FOUND

[Gas-16] Using private rather than public for constants and immutables, saves gas

Resolution

Using private visibility for constants and immutables in Solidity instead of public can save gas. This is because private elements are not included in the contract's ABI, reducing the deployment and interaction costs. To achieve better efficiency, it is recommended to use private visibility when external access is not needed.

Num of instances: 8

Findings

Click to show findings

['112']

112: bytes32 public constant DaoCollateralStorageV0Location = // <= FOUND

['29']

29: bytes32 public constant RegistryContractStorageV0Location = // <= FOUND

['99']

99: bytes32 public constant SwapperEngineStorageV0Location = // <= FOUND

['39']

39: bytes32 public constant TokenMappingStorageV0Location = // <= FOUND

['41']

41: bytes32 public constant Usd0StorageV0Location = // <= FOUND

['79']

79: bytes32 public constant Usd0PPStorageV0Location = // <= FOUND

['31']

31: bytes32 public constant UsualOracleStorageV0Location = // <= FOUND

['61']

61: bytes32 public constant AbstractOracleStorageV0Location = // <= FOUND

[Gas-17] Mark Functions That Revert For Normal Users As payable

Resolution

In Solidity, marking functions as payable allows them to accept Ether. If a function is known to revert for regular users (non-admin or specific roles) but needs to be accessible to others, marking it as payable can be beneficial. This ensures that even if a regular user accidentally sends Ether to the function, the Ether won't be trapped, as the function reverts, returning the funds. This can save gas by avoiding unnecessary failure handling in the function itself. Resolution: Carefully assess the roles and access patterns, and mark functions that should revert for regular users as payable to handle accidental Ether transfers.

Num of instances: 7