Platform: Code4rena
Start Date: 19/01/2024
Pot Size: $36,500 USDC
Total HM: 9
Participants: 113
Period: 3 days
Judge: 0xsomeone
Id: 322
League: ETH
Rank: 25/113
Findings: 2
Award: $204.24
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Kaysoft
Also found by: 0xmystery, Aamir, DadeKuma, IceBear, Pechenite, SBSecurity, Shaheen, bronze_pickaxe, ether_sky, nobody2018, rjs, rouhsamad, slvDev, zxriptor
12.2818 USDC - $12.28
Issue | Instances | |
---|---|---|
[L-01] | Potential Gas Griefing Due to Non-Handling of Return Data in External Calls | 2 |
[L-02] | .call bypasses function existence check, type checking and argument packing | 5 |
[L-03] | Unbounded Gas Consumption on External Calls | 5 |
[L-04] | Use of ecrecover is susceptible to signature malleability | 1 |
[L-05] | Upgradable contracts not taken into account | 20 |
[L-06] | Risk of Permanently Locked Ether | 12 |
[L-07] | Missing Contract-Existence Checks Before Low-Level Calls | 7 |
[L-08] | Loss of precision | 2 |
[L-09] | Missing address(0) Check in Constructor | 3 |
Issue | Instances | |
---|---|---|
[N-01] | Variables need not be initialized to zero | 5 |
[N-02] | require()/revert() statements without reason strings | 1 |
[N-03] | Consider using descriptive constant s when passing zero as a function argument | 1 |
[N-04] | Multiple address /ID mappings can be combined into a single mapping of an address /ID to a struct , for readability | 6 |
[N-05] | Remove Unused private Functions | 1 |
[N-06] | High cyclomatic complexity | 2 |
[N-07] | State-Altering Functions Should Emit Events | 12 |
[N-08] | Contract/Libraries Names Do Not Match Their Filenames | 1 |
[N-09] | Function/Constructor Argument Names Not in mixedCase | 26 |
[N-10] | Contract/Library Names Not in CapWords Style (CamelCase) | 1 |
[N-11] | Leverage Recent Solidity Features with 0.8.23 | 11 |
Due to the EVM architecture, return data (bool success,) has to be stored. However, when 'out' and 'outsize' values are given (0,0), this storage disappears. This can lead to potential gas griefing/theft issues, especially when dealing with external contracts.
assembly { success: = call(gas(), dest, amount, 0, 0) } require(success, "transfer failed");
Consider using a safe call pattern above to avoid these issues. The following instances show the unsafe external call patterns found in the code.
<details> <summary><i>2 issue instances in 1 files:</i></summary></details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 33: (bool success, ) = target.call(callPayload); 61: (bool success, ) = target.call{value: amount}(callPayload);
.call
bypasses function existence check, type checking and argument packingUsing the .call
method in Solidity enables direct communication with an address, bypassing function existence checks, type checking, and argument packing.
While this can save gas and provide flexibility, it can also introduce security risks and potential errors. The absence of these checks can lead to unexpected behavior if the callee contract's interface changes or if the input parameters are not crafted with care.
The resolution to these issues is to use Solidity's high-level interface for calling functions when possible, as it automatically manages these aspects.
If using .call
is necessary, ensure that the inputs are carefully validated and that awareness of the called contract's behavior is maintained.
File: src/UTBExecutor.sol 52: target.call{value: amount}(payload) 65: target.call{value: extraNative}(payload) 70: target.call(payload)
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 33: target.call(callPayload) 61: target.call{value: amount}(callPayload)
External calls in your code don't specify a gas limit, which can lead to scenarios where the recipient consumes all transaction's gas causing it to revert.
Consider using addr.call{gas: <amount>}("")
to set a gas limit and prevent potential reversion due to gas consumption.
File: src/UTBExecutor.sol 52: (success, ) = target.call{value: amount}(payload); 65: (success, ) = target.call{value: extraNative}(payload); 70: (success, ) = target.call(payload);
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 33: (bool success, ) = target.call(callPayload); 61: (bool success, ) = target.call{value: amount}(callPayload);
ecrecover
is susceptible to signature malleabilityThe built-in EVM precompile ecrecover is susceptible to signature malleability, which could lead to replay attacks. References: https://swcregistry.io/docs/SWC-117, https://swcregistry.io/docs/SWC-121. While this is not immediately exploitable, this may become a vulnerability if used elsewhere. Consider using OpenZeppelin’s ECDSA library (which prevents this malleability) instead of the built-in function.
<details> <summary><i>1 issue instances in 1 files:</i></summary></details>File: src/UTBFeeCollector.sol 53: ecrecover(constructedHash, v, r, s)
In the realm of blockchain development, it's crucial to consider the impact of upgradable contracts, especially when handling token addresses through interfaces like IERC20. These contracts can evolve over time, potentially altering their behavior or interface. Such changes may lead to compatibility issues or security vulnerabilities in the protocol that relies on them.
<details> <summary><i>20 issue instances in 6 files:</i></summary>File: src/UTB.sol 83: IERC20(swapParams.tokenIn).transferFrom( 90: IERC20(swapParams.tokenIn).approve( 152: IERC20(tokenOut).approve(address(executor), amountOut); 236: IERC20(fees.feeToken).transferFrom( 241: IERC20(fees.feeToken).approve(
File: src/UTBExecutor.sol 61: IERC20(token).transferFrom(msg.sender, address(this), amount); 62: IERC20(token).approve(paymentOperator, amount); 80: IERC20(token).transfer(refund, remainingBalance);
File: src/UTBFeeCollector.sol 56: IERC20(fees.feeToken).transferFrom( 73: IERC20(token).transfer(owner, amount);
File: src/bridge_adapters/DecentBridgeAdapter.sol 139: IERC20(swapParams.tokenIn).transferFrom( 145: IERC20(swapParams.tokenIn).approve(utb, swapParams.amountIn);
File: src/bridge_adapters/StargateBridgeAdapter.sol 207: IERC20(swapParams.tokenIn).approve(utb, swapParams.amountIn);
</details>File: src/swappers/UniSwapper.sol 44: IERC20(token).transfer(user, amount); 55: IERC20(token).transfer(recipient, amount); 83: IERC20(swapParams.tokenIn).transferFrom( 137: IERC20(swapParams.tokenIn).approve(uniswap_router, swapParams.amountIn); 158: IERC20(swapParams.tokenIn).approve(uniswap_router, swapParams.amountIn);
When Ether is mistakenly sent to a contract without a means of retrieval, it becomes irrevocably locked. Incidents of accidental Ether transfers have been observed even in high-profile projects, potentially leading to significant financial setbacks. To enhance contract resilience, it's recommended to incorporate an "Ether recovery" function to serve as a protective measure against unintended Ether lockups.
<details> <summary><i>12 issue instances in 6 files:</i></summary>File: src/UTB.sol 339: receive() external payable {} 341: fallback() external payable {}
File: src/UTBFeeCollector.sol 77: receive() external payable {} 79: fallback() external payable {}
File: src/bridge_adapters/DecentBridgeAdapter.sol 156: receive() external payable {} 158: fallback() external payable {}
File: src/bridge_adapters/StargateBridgeAdapter.sol 218: receive() external payable {} 220: fallback() external payable {}
File: src/swappers/UniSwapper.sol 171: receive() external payable {} 173: fallback() external payable {}
</details>File: lib/decent-bridge/src/DecentEthRouter.sol 337: receive() external payable {} 339: fallback() external payable {}
When making low-level calls, it's crucial to ensure the existence of the contract at the specified address. If the contract doesn't exist at the given address, low-level calls will still return success, potentially causing errors in the code execution. Therefore, alongside zero-address checks, adding an additional check to verify that <address>.code.length > 0 before making low-level calls would be recommended.
<details> <summary><i>7 issue instances in 2 files:</i></summary>File: src/UTBExecutor.sol 52: (success, ) = target.call{value: amount}(payload); 54: (refund.call{value: amount}("")); 65: (success, ) = target.call{value: extraNative}(payload); 67: (refund.call{value: extraNative}("")); 70: (success, ) = target.call(payload);
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 33: (bool success, ) = target.call(callPayload); 61: (bool success, ) = target.call{value: amount}(callPayload);
Division by large numbers may result in the result being zero, due to Solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator.
<details> <summary><i>2 issue instances in 1 files:</i></summary></details>File: src/bridge_adapters/StargateBridgeAdapter.sol 66: return (amt2Bridge * (1e4 - SG_FEE_BPS)) / 1e4; 176: (amt2Bridge * (10000 - SG_FEE_BPS)) / 10000, // the min qty you would accept on the destination, fee is 6 bips
address(0)
Check in ConstructorThe constructor does not include a check for address(0)
when set state variables that hold addresses.
Initializing a state variable with address(0)
can lead to unintended behavior and vulnerabilities in the contract, such as sending funds to an inaccessible address.
It is recommended to include a validation step to ensure that address parameters are not set to address(0)
.
File: src/bridge_adapters/DecentBridgeAdapter.sol /// @audit `_bridgeToken` has lack of `address(0)` check before use 20: constructor(bool _gasIsEth, address _bridgeToken) BaseAdapter() {
File: lib/decent-bridge/src/DecentEthRouter.sol /// @audit `_wethAddress` has lack of `address(0)` check before use /// @audit `_executor` has lack of `address(0)` check before use 26: constructor( address payable _wethAddress, bool gasIsEth, address _executor ) Owned(msg.sender) {
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol /// @audit `_weth` has lack of `address(0)` check before use 11: constructor(address _weth, bool gasIsEth) Owned(msg.sender) {
By default, int/uint
variables in Solidity are initialized to zero
.
Explicitly setting variables to zero during their declaration is redundant and might cause confusion.
Removing the explicit zero initialization can improve code readability and understanding.
File: src/UTB.sol 234: uint value = 0;
File: src/bridge_adapters/DecentBridgeAdapter.sol 13: uint8 public constant BRIDGE_ID = 0;
File: src/swappers/SwapParams.sol 5: uint8 constant EXACT_IN = 0;
File: src/swappers/UniSwapper.sol 16: uint8 public constant SWAPPER_ID = 0;
</details>File: lib/decent-bridge/src/DecentEthRouter.sol 17: uint8 public constant MT_ETH_TRANSFER = 0;
require()/revert()
statements without reason stringsIn Solidity, require() and revert() functions can include an optional 'reason' string that describes what caused the function to fail. This string can be incredibly helpful during testing and debugging, as it provides more context for what went wrong.
Some require()/revert()
statements do not have these descriptive reason strings. For better error handling and debugging, it is strongly recommended to add descriptive reason strings to all require()/revert()
statements.
</details>File: lib/decent-bridge/src/DcntEth.sol 9: require(msg.sender == router)
constant
s when passing zero as a function argumentIn instances where utilizing a zero parameter is essential, it is recommended to employ descriptive constants or an enum instead of directly integrating zero within function calls. This strategy aids in clearly articulating the caller's intention and minimizes the risk of errors. Emitting zero also not recomended, as it is not clear what the intention is.
<details> <summary><i>1 issue instances in 1 files:</i></summary></details>File: src/UTBExecutor.sol 28: execute(target, paymentOperator, payload, token, amount, refund, 0)
address
/ID mappings can be combined into a single mapping
of an address
/ID to a struct
, for readabilityCombining multiple address/ID mappings into a single mapping to a struct can enhance code clarity and maintainability. Consider refactoring multiple mappings into a single mapping with a struct for cleaner code structure. This arrangement also promotes a more organized contract structure, making it easier for developers to navigate and understand.
<details> <summary><i>6 issue instances in 3 files:</i></summary>File: src/UTB.sol 21: mapping(uint8 => address) public swappers 22: mapping(uint8 => address) public bridgeAdapters
File: src/bridge_adapters/DecentBridgeAdapter.sol 14: mapping(uint256 => address) public destinationBridgeAdapter 16: mapping(uint256 => uint16) lzIdLookup
</details>File: src/bridge_adapters/StargateBridgeAdapter.sol 25: mapping(uint256 => address) public destinationBridgeAdapter 26: mapping(uint256 => uint16) lzIdLookup
private
Functionsprivate
functions that are not called anywhere within the same contract are redundant and should be removed to save deployment gas.
Unlike public
and internal
functions, private
functions cannot be called by derived contracts or externally, they can only be called within the contract they are defined.
</details>File: src/bridge_adapters/StargateBridgeAdapter.sol 100: function getValue( bytes calldata additionalArgs, uint256 amt2Bridge ) private view returns (uint value)
Functions with high cyclomatic complexity are harder to understand, test, and maintain. Consider breaking down these blocks into more manageable units, by splitting things into utility functions, by reducing nesting, and by using early returns.
Learn More About Cyclomatic Complexity
<details> <summary><i>2 issue instances in 2 files:</i></summary>File: src/UTBExecutor.sol /// @audit function `execute` has a cyclomatic complexity of 6 41: function execute( address target, address paymentOperator, bytes memory payload, address token, uint amount, address payable refund, uint extraNative ) public onlyOwner {
</details>File: lib/decent-bridge/src/DecentEthRouter.sol /// @audit function `onOFTReceived` has a cyclomatic complexity of 6 237: function onOFTReceived( uint16 _srcChainId, bytes calldata, uint64, bytes32, uint _amount, bytes memory _payload ) external override onlyLzApp {
Functions that alter state should emit events to inform users of the state change. This is crucial for functions that modify the state and don't return a value. The absence of events in such scenarios could lead to lack of transparency and traceability, undermining the contract's reliability.
<details> <summary><i>12 issue instances in 8 files:</i></summary>File: src/UTB.sol 29: executor = IUTBExecutor(_executor); 37: wrapped = IWETH(_wrapped); 45: feeCollector = IUTBFeeCollector(_feeCollector);
File: src/UTBFeeCollector.sol 19: signer = _signer;
File: src/bridge_adapters/BaseAdapter.sol 20: bridgeExecutor = _executor;
File: src/bridge_adapters/DecentBridgeAdapter.sol 27: router = IDecentEthRouter(payable(_router));
File: src/bridge_adapters/StargateBridgeAdapter.sol 34: router = IStargateRouter(_router); 38: stargateEth = _sgEth;
File: src/swappers/UniSwapper.sol 21: uniswap_router = _router; 25: wrapped = _wrapped;
File: lib/decent-bridge/src/DcntEth.sol 21: router = _router;
</details>File: lib/decent-bridge/src/DecentEthRouter.sol 69: dcntEth = IDcntEth(_addr);
According to the Solidity Style Guide, contract names should match their filenames. Mismatching contract names and filenames can lead to confusion and make the code harder to maintain and review.
<details> <summary><i>1 issue instances in 1 files:</i></summary></details>File: src/swappers/SwapParams.sol 3: library SwapDirection {
Underscore before of after function argument names is a common convention in Solidity NOT a documentation requirement.
Function arguments should use mixedCase for better readability and consistency with Solidity style guidelines. Examples of good practice include: initialSupply, account, recipientAddress, senderAddress, newOwner. More information in Documentation
Rule exceptions
_
at the beginning of the mixedCase match for private variables
and unused parameters
.File: src/UTB.sol 28: function setExecutor(address _executor) public onlyOwner { 36: function setWrapped(address payable _wrapped) public onlyOwner { 44: function setFeeCollector(address payable _feeCollector) public onlyOwner {
File: src/UTBFeeCollector.sol 18: function setSigner(address _signer) public onlyOwner {
File: src/bridge_adapters/BaseAdapter.sol 18: function setBridgeExecutor(address _executor) public onlyOwner {
File: src/bridge_adapters/DecentBridgeAdapter.sol 25: function setRouter(address _router) public onlyOwner { 20: constructor(bool _gasIsEth, address _bridgeToken) BaseAdapter() {
File: src/bridge_adapters/StargateBridgeAdapter.sol 32: function setRouter(address _router) public onlyOwner { 36: function setStargateEth(address _sgEth) public onlyOwner {
File: src/swappers/UniSwapper.sol 19: function setRouter(address _router) public onlyOwner { 23: function setWrapped(address payable _wrapped) public onlyOwner {
File: lib/decent-bridge/src/DcntEth.sol 20: function setRouter(address _router) public { 23: function mint(address _to, uint256 _amount) public onlyRouter { 27: function burn(address _from, uint256 _amount) public onlyRouter { 31: function mintByOwner(address _to, uint256 _amount) public onlyOwner { 35: function burnByOwner(address _from, uint256 _amount) public onlyOwner {
File: lib/decent-bridge/src/DecentEthRouter.sol 68: function registerDcntEth(address _addr) public onlyOwner { 73: function addDestinationBridge( uint16 _dstChainId, address _routerAddress ) public onlyOwner { 79: function _getCallParams( uint8 msgType, address _toAddress, uint16 _dstChainId, uint64 _dstGasForCall, bool deliverEth, bytes memory additionalPayload ) private view returns ( bytes32 destBridge, bytes memory adapterParams, bytes memory payload ) { 112: function estimateSendAndCallFee( uint8 msgType, uint16 _dstChainId, address _toAddress, uint _amount, uint64 _dstGasForCall, bool deliverEth, bytes memory payload ) public view returns (uint nativeFee, uint zroFee) { 147: function _bridgeWithPayload( uint8 msgType, uint16 _dstChainId, address _toAddress, uint _amount, uint64 _dstGasForCall, bytes memory additionalPayload, bool deliverEth ) internal { 197: function bridgeWithPayload( uint16 _dstChainId, address _toAddress, uint _amount, bool deliverEth, uint64 _dstGasForCall, bytes memory additionalPayload ) public payable { 218: function bridge( uint16 _dstChainId, address _toAddress, uint _amount, uint64 _dstGasForCall, bool deliverEth // if false, delivers WETH ) public payable { 237: function onOFTReceived( uint16 _srcChainId, bytes calldata, uint64, bytes32, uint _amount, bytes memory _payload ) external override onlyLzApp { 26: constructor( address payable _wethAddress, bool gasIsEth, address _executor ) Owned(msg.sender) {
68 | 73 | 79 | 112 | 147 | 197 | 218 | 237 | 26
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 11: constructor(address _weth, bool gasIsEth) Owned(msg.sender) {
CapWords
Style (CamelCase)Contracts and libraries should be named using the CapWords
style for better readability and consistency with Solidity style guidelines.
Examples of good practice include: SimpleToken, SmartBank, CertificateHashRepository, Player, Congress, Owned.
More information in Documentation
</details>File: src/UTB.sol 13: contract UTB is Owned {
0.8.23
The recent updates in Solidity provide several features and optimizations that, when leveraged appropriately, can significantly improve your contract's code clarity and maintainability. Key enhancements include the use of push0 for placing 0 on the stack for EVM versions starting from "Shanghai", making your code simpler and more straightforward. Moreover, Solidity has extended NatSpec documentation support to enum and struct definitions, facilitating more comprehensive and insightful code documentation.
Additionally, the re-implementation of the UnusedAssignEliminator and UnusedStoreEliminator in the Solidity optimizer provides the ability to remove unused assignments in deeply nested loops. This results in a cleaner, more efficient contract code, reducing clutter and potential points of confusion during code review or debugging. It's recommended to make full use of these features and optimizations to enhance the robustness and readability of your smart contracts.
<details> <summary><i>11 issue instances in 11 files:</i></summary>File: src/UTB.sol 2: pragma solidity ^0.8.0;
| Line #2 |
File: src/UTBExecutor.sol 2: pragma solidity ^0.8.0;
| Line #2 |
File: src/UTBFeeCollector.sol 2: pragma solidity ^0.8.0;
| Line #2 |
File: src/bridge_adapters/BaseAdapter.sol 2: pragma solidity ^0.8.0;
| Line #2 |
File: src/bridge_adapters/DecentBridgeAdapter.sol 2: pragma solidity ^0.8.0;
| Line #2 |
File: src/bridge_adapters/StargateBridgeAdapter.sol 2: pragma solidity ^0.8.0;
| Line #2 |
File: src/swappers/SwapParams.sol 2: pragma solidity ^0.8.0;
| Line #2 |
File: src/swappers/UniSwapper.sol 2: pragma solidity ^0.8.0;
| Line #2 |
File: lib/decent-bridge/src/DcntEth.sol 2: pragma solidity ^0.8.13;
| Line #2 |
File: lib/decent-bridge/src/DecentEthRouter.sol 2: pragma solidity ^0.8.13;
| Line #2 |
File: lib/decent-bridge/src/DecentBridgeExecutor.sol 2: pragma solidity ^0.8.0;
| Line #2 | </details>
#0 - raymondfam
2024-01-26T05:41:58Z
Generic findings with most of them already known from the bot(s).
#1 - c4-pre-sort
2024-01-26T05:42:03Z
raymondfam marked the issue as sufficient quality report
#2 - alex-ppg
2024-02-04T22:47:29Z
The Warden's QA report has been graded B based on a score of 29
combined with a manual review per the relevant QA guideline document located here.
The Warden's submission's score was assessed based on the following accepted findings:
#3 - c4-judge
2024-02-04T22:47:32Z
alex-ppg marked the issue as grade-b
#4 - alex-ppg
2024-02-04T22:52:22Z
To note, this submission was tied with #616 and #542 for an A
grade per the relevant guidelines shared above. I opted to retain #616 as the A
grade report due to its more "manual" nature in comparison to #542 (this report) and #604 which contain a lot of bot-related findings and follow the format of statically generated findings.
🌟 Selected for report: c3phas
Also found by: 0x11singh99, Raihan, dharma09, hunter_w3b, slvDev
191.9635 USDC - $191.96
Issue | Instances | Total Gas Saved | |
---|---|---|---|
[G-01] | Multiple address /ID mappings can be combined into a single mapping of an address /ID to a struct | 6 | - |
[G-02] | Stack variable is only used once | 5 | 48 |
[G-03] | Optimize require/revert Statements with Assembly | 12 | 3600 |
[G-04] | Use Cached Contracts for Multiple External Calls | 17 | 2576 |
[G-05] | Optimize Gas by Using Only Named Returns | 14 | 616 |
[G-06] | Use bytes32 in place of string | 2 | 0 |
[G-07] | Use Assembly for Hash Calculations | 2 | 2010 |
[G-08] | Avoid Inverting if -else Conditions | 2 | 6 |
[G-09] | Optimize External Calls with Assembly for Memory Efficiency | 14 | 457 |
[G-10] | Unlimited gas consumption risk due to external call recipients | 5 | - |
[G-11] | Reduce deployment costs by tweaking contracts' metadata | 11 | 116600 |
[G-12] | Unused private functions should be removed to save deployment gas | 1 | 0 |
[G-13] | Use revert() to gain maximum gas savings | 12 | 600 |
[G-14] | Optimize Ether Transfers with receive() Function | 13 | 585 |
[G-15] | Avoid contract existence checks by using low-level calls | 47 | 4700 |
[G-16] | Usage of uints /ints smaller than 32 bytes (256 bits) incurs overhead | 31 | 198 |
address
/ID mappings can be combined into a single mapping
of an address
/ID to a struct
For destinationBridgeAdapter
and lzIdLookup
used dstChainId
as a key in registerRemoteBridgeAdapter()
function so it can be combined into a single mapping
of an address
/ID to a struct
to save gas.
File: src/bridge_adapters/DecentBridgeAdapter.sol 14: mapping(uint256 => address) public destinationBridgeAdapter 16: mapping(uint256 => uint16) lzIdLookup
</details>File: src/bridge_adapters/StargateBridgeAdapter.sol 25: mapping(uint256 => address) public destinationBridgeAdapter 26: mapping(uint256 => uint16) lzIdLookup
If the variable is only accessed once, it's cheaper to use the assigned value directly that one time, and save the 3 gas the extra stack assignment would spend
<details> <summary><i>5 issue instances in 2 files:</i></summary>File: src/UTB.sol /// @audit - `native` variable 287: bool native = approveAndCheckIfNative(instructions, amt2Bridge) /// @audit - `s` variable 326: ISwapper s = ISwapper(swapper) /// @audit - `b` variable 335: IBridgeAdapter b = IBridgeAdapter(bridge)
</details>File: lib/decent-bridge/src/DecentEthRouter.sol /// @audit - `GAS_FOR_RELAY` variable 96: uint256 GAS_FOR_RELAY = 100000 /// @audit - `gasAmount` variable 97: uint256 gasAmount = GAS_FOR_RELAY + _dstGasForCall /// @audit - `callParams` variable
require/revert
Statements with AssemblyUsing inline assembly for revert statements in Solidity can offer gas optimizations.
The typical require
or revert
statements in Solidity perform additional memory operations and type checks which can be avoided by using low-level assembly code.
In certain contracts, particularly those that might revert often or are otherwise sensitive to gas costs, using assembly to handle reversion can offer meaningful savings. These savings primarily come from avoiding memory expansion costs and extra type checks that the Solidity compiler performs.
Example:
<details> <summary><i>12 issue instances in 8 files:</i></summary>/// calling restrictedAction(2) with a non-owner address: 24042 function restrictedAction(uint256 num) external { require(owner == msg.sender, "caller is not owner"); specialNumber = num; } // calling restrictedAction(2) with a non-owner address: 23734 function restrictedAction(uint256 num) external { assembly { if sub(caller(), sload(owner.slot)) { mstore(0x00, 0x20) // store offset to where length of revert message is stored mstore(0x20, 0x13) // store length (19) mstore(0x40, 0x63616c6c6572206973206e6f74206f776e657200000000000000000000000000) // store hex representation of message revert(0x00, 0x60) // revert with data } } specialNumber = num; }
File: src/UTB.sol 75: require(msg.value >= swapParams.amountIn, "not enough native");
File: src/UTBFeeCollector.sol 29: require(signature.length == 65, "Invalid signature length"); 54: require(recovered == signer, "Wrong signature");
File: src/bridge_adapters/BaseAdapter.sol 12: require( msg.sender == address(bridgeExecutor), "Only bridge executor can call this" );
File: src/bridge_adapters/DecentBridgeAdapter.sol 91: require( destinationBridgeAdapter[dstChainId] != address(0), string.concat("dst chain address not set ") );
File: src/bridge_adapters/StargateBridgeAdapter.sol 157: require( dstAddr != address(0), string.concat("dst chain address not set ") );
File: src/swappers/UniSwapper.sol 96: require(uniswap_router != address(0), "router not set");
File: lib/decent-bridge/src/DcntEth.sol 9: require(msg.sender == router);
</details>File: lib/decent-bridge/src/DecentEthRouter.sol 38: require(gasCurrencyIsEth, "Gas currency is not ETH"); 43: require( address(dcntEth) == msg.sender, "DecentEthRouter: only lz App can call" ); 51: require(weth.balanceOf(address(this)) > amount, "not enough reserves"); 62: require(balance >= amount, "not enough balance");
When function makes multiple calls to the same external contract, it is more gas-efficient to use a local copy of the contract. This is because the EVM will cache the contract in memory, and subsequent calls will be cheaper. It's especially true for contracts that are large and/or have many functions.
<details> <summary><i>17 issue instances in 4 files:</i></summary>// local cache -> 6561 gas IToken localCache = storageContract; localCache.externalCall(); localCache.externalCall(); // direct call 6683 gas storageContract.externalCall(); storageContract.externalCall();
File: src/UTB.sol /// @audit function performSwap() make external call of `wrapped` - 2 times 76: wrapped.deposit{value: swapParams.amountIn}(); 98: wrapped.withdraw(amountOut); /// @audit function _swapAndExecute() make external call of `executor` - 2 times 143: executor.execute{value: amountOut}( 153: executor.execute(
File: src/bridge_adapters/DecentBridgeAdapter.sol /// @audit function estimateFees() make external call of `router` - 2 times 56: router.estimateSendAndCallFee( 57: router.MT_ETH_TRANSFER_WITH_PAYLOAD(),
File: lib/decent-bridge/src/DecentEthRouter.sol /// @audit function _bridgeWithPayload() make external call of `weth` - 2 times 178: weth.deposit{value: _amount}(); 181: weth.transferFrom(msg.sender, address(this), _amount); /// @audit function onOFTReceived() make external call of `weth` - 4 times 266: if (weth.balanceOf(address(this)) < _amount) { 273: weth.transfer(_to, _amount); 275: weth.withdraw(_amount); 279: weth.approve(address(executor), _amount);
178 | 181 | 266 | 273 | 275 | 279
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol /// @audit function _executeWeth() make external call of `weth` - 5 times 30: uint256 balanceBefore = weth.balanceOf(address(this)); 31: weth.approve(target, amount); 36: weth.transfer(from, amount); 41: (balanceBefore - weth.balanceOf(address(this))); 44: weth.transfer(from, remainingAfterCall);
The Solidity compiler can generate more efficient bytecode when using named returns. It's recommended to replace anonymous returns with named returns for potential gas savings.
Example:
<details> <summary><i>14 issue instances in 4 files:</i></summary>/// 985 gas cost function add(uint256 x, uint256 y) public pure returns (uint256) { return x + y; } /// 941 gas cost function addNamed(uint256 x, uint256 y) public pure returns (uint256 res) { res = x + y; }
File: src/UTB.sol 207: function approveAndCheckIfNative( BridgeInstructions memory instructions, uint256 amt2Bridge ) private returns (bool) { 259: function bridgeAndExecute( BridgeInstructions calldata instructions, FeeStructure calldata fees, bytes calldata signature ) public payable retrieveAndCollectFees(fees, abi.encode(instructions, fees), signature) returns (bytes memory) { 282: function callBridge( uint256 amt2Bridge, uint bridgeFee, BridgeInstructions memory instructions ) private returns (bytes memory) {
File: src/bridge_adapters/DecentBridgeAdapter.sol 29: function getId() public pure returns (uint8) { 66: function getBridgeToken( bytes calldata /*additionalArgs*/ ) external view returns (address) { 72: function getBridgedAmount( uint256 amt2Bridge, address /*tokenIn*/, address /*tokenOut*/ ) external pure returns (uint256) {
File: src/bridge_adapters/StargateBridgeAdapter.sol 40: function getId() public pure returns (uint8) { 60: function getBridgedAmount( uint256 amt2Bridge, address /*tokenIn*/, address /*tokenOut*/ ) external pure returns (uint256) { 113: function getLzTxObj( bytes calldata additionalArgs ) private pure returns (IStargateRouter.lzTxObj memory) { 123: function getDstChainId( bytes calldata additionalArgs ) private pure returns (uint16) { 133: function getSrcPoolId( bytes calldata additionalArgs ) private pure returns (uint120) { 143: function getDstPoolId( bytes calldata additionalArgs ) private pure returns (uint120) {
40 | 60 | 113 | 123 | 133 | 143
</details>File: src/swappers/UniSwapper.sol 27: function getId() public pure returns (uint8) { 31: function updateSwapParams( SwapParams memory newSwapParams, bytes memory payload ) external pure returns (bytes memory) {
For strings of 32 char strings and below you can use bytes32 instead as it's more gas efficient
<details> <summary><i>2 issue instances in 2 files:</i></summary>File: src/bridge_adapters/DecentBridgeAdapter.sol 93: string.concat("dst chain address not set ")
</details>File: src/bridge_adapters/StargateBridgeAdapter.sol 159: string.concat("dst chain address not set ")
In certain cases, using inline assembly to calculate hashes can lead to significant gas savings. Solidity's built-in keccak256 function is convenient but costs more gas than the equivalent assembly code. However, it's important to note that using assembly should be done with care as it's less readable and could increase the risk of introducing errors.
<details> <summary><i>2 issue instances in 1 files:</i></summary></details>File: src/UTBFeeCollector.sol 49: bytes32 constructedHash = keccak256( 50: abi.encodePacked(BANNER, keccak256(packedInfo))
if
-else
ConditionsInverting the condition of an if
-else
-statement results in extra gas consumption.
Consider simplifying the condition to reduce gas costs.
File: lib/decent-bridge/src/DecentEthRouter.sol 272: if (!gasCurrencyIsEth || !deliverEth) { weth.transfer(_to, _amount); } else { weth.withdraw(_amount); payable(_to).transfer(_amount); }
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 76: if (!gasCurrencyIsEth || !deliverEth) { _executeWeth(from, target, amount, callPayload); } else { _executeEth(from, target, amount, callPayload); }
Using interfaces to make external contract calls in Solidity is convenient but can be inefficient in terms of memory utilization. Each such call involves creating a new memory location to store the data being passed, thus incurring memory expansion costs.
Inline assembly allows for optimized memory usage by re-using already allocated memory spaces or using the scratch space for smaller datasets. This can result in notable gas savings, especially for contracts that make frequent external calls.
Additionally, using inline assembly enables important safety checks like verifying if the target address has code deployed to it using extcodesize(addr)
before making the call, mitigating risks associated with contract interactions.
File: src/UTB.sol 152: IERC20(tokenOut).approve(address(executor), amountOut); 216: IERC20(bridgeToken).approve(address(bridgeAdapter), amt2Bridge);
File: src/UTBExecutor.sol 61: IERC20(token).transferFrom(msg.sender, address(this), amount); 62: IERC20(token).approve(paymentOperator, amount); 73: uint remainingBalance = IERC20(token).balanceOf(address(this)) - initBalance; 80: IERC20(token).transfer(refund, remainingBalance);
File: src/bridge_adapters/DecentBridgeAdapter.sol 109: IERC20(bridgeToken).transferFrom( msg.sender, address(this), amt2Bridge ); 114: IERC20(bridgeToken).approve(address(router), amt2Bridge);
File: src/bridge_adapters/StargateBridgeAdapter.sol 88: IERC20(bridgeToken).transferFrom(msg.sender, address(this), amt2Bridge); 89: IERC20(bridgeToken).approve(address(router), amt2Bridge);
</details>File: src/swappers/UniSwapper.sol 44: IERC20(token).transfer(user, amount); 55: IERC20(token).transfer(recipient, amount); 138: amountOut = IV3SwapRouter(uniswap_router).exactInput(params); 159: amountIn = IV3SwapRouter(uniswap_router).exactOutput(params);
When calling an external function without specifying a gas limit , the called contract may consume all the remaining gas, causing the tx to be reverted. To mitigate this, it is recommended to explicitly set a gas limit when making low level external calls.
<details> <summary><i>5 issue instances in 2 files:</i></summary>File: src/UTBExecutor.sol 52: (success, ) = target.call{value: amount}(payload); 65: (success, ) = target.call{value: extraNative}(payload); 70: (success, ) = target.call(payload);
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 33: (bool success, ) = target.call(callPayload); 61: (bool success, ) = target.call{value: amount}(callPayload);
The Solidity compiler appends 53 bytes of metadata to the smart contract code which translates to an extra 10,600 gas (200 per bytecode) + the calldata cost (16 gas per non-zero bytes, 4 gas per zero-byte). This translates to up to 848 additional gas in calldata cost. One way to reduce this cost is by optimizing the IPFS hash that gets appended to the smart contract code.
Why is this important?
Options to Reduce Gas:
--no-cbor-metadata
compiler option to exclude metadata, but this might affect contract verification.File: src/UTB.sol 1: Consider optimizing the IPFS hash during deployment.
File: src/UTBExecutor.sol 1: Consider optimizing the IPFS hash during deployment.
File: src/UTBFeeCollector.sol 1: Consider optimizing the IPFS hash during deployment.
File: src/bridge_adapters/BaseAdapter.sol 1: Consider optimizing the IPFS hash during deployment.
File: src/bridge_adapters/DecentBridgeAdapter.sol 1: Consider optimizing the IPFS hash during deployment.
File: src/bridge_adapters/StargateBridgeAdapter.sol 1: Consider optimizing the IPFS hash during deployment.
File: src/swappers/SwapParams.sol 1: Consider optimizing the IPFS hash during deployment.
File: src/swappers/UniSwapper.sol 1: Consider optimizing the IPFS hash during deployment.
File: lib/decent-bridge/src/DcntEth.sol 1: Consider optimizing the IPFS hash during deployment.
File: lib/decent-bridge/src/DecentEthRouter.sol 1: Consider optimizing the IPFS hash during deployment.
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 1: Consider optimizing the IPFS hash during deployment.
private
functions should be removed to save deployment gasAll private functions that are never used can be safely removed or commented out to save gas.
<details> <summary><i>1 issue instances in 1 files:</i></summary></details>File: src/bridge_adapters/StargateBridgeAdapter.sol 100: function getValue( bytes calldata additionalArgs, uint256 amt2Bridge ) private view returns (uint value)
revert()
to gain maximum gas savingsIf you dont need Error messages, or you want gain maximum gas savings - revert()
is a cheapest way to revert transaction in terms of gas.
<details> <summary><i>12 issue instances in 8 files:</i></summary>revert(); // 117 gas require(false); // 132 gas revert CustomError(); // 157 gas assert(false); // 164 gas revert("Custom Error"); // 406 gas require(false, "Custom Error"); // 421 gas
File: src/UTB.sol 75: require(msg.value >= swapParams.amountIn, "not enough native")
File: src/UTBFeeCollector.sol 29: require(signature.length == 65, "Invalid signature length") 54: require(recovered == signer, "Wrong signature")
File: src/bridge_adapters/BaseAdapter.sol 12: require( msg.sender == address(bridgeExecutor), "Only bridge executor can call this" )
File: src/bridge_adapters/DecentBridgeAdapter.sol 91: require( destinationBridgeAdapter[dstChainId] != address(0), string.concat("dst chain address not set ") )
File: src/bridge_adapters/StargateBridgeAdapter.sol 157: require( dstAddr != address(0), string.concat("dst chain address not set ") )
File: src/swappers/UniSwapper.sol 96: require(uniswap_router != address(0), "router not set")
File: lib/decent-bridge/src/DcntEth.sol 9: require(msg.sender == router)
</details>File: lib/decent-bridge/src/DecentEthRouter.sol 38: require(gasCurrencyIsEth, "Gas currency is not ETH") 43: require( address(dcntEth) == msg.sender, "DecentEthRouter: only lz App can call" ) 51: require(weth.balanceOf(address(this)) > amount, "not enough reserves") 62: require(balance >= amount, "not enough balance")
receive()
FunctionConsider using receive()
function instead of a specific deposit()
(or similar) function.
If there are several functions in the contract that can receive Ether, it is recommended to use receive()
for the most frequently used function.
function deposit() external payable { // 5401 gas // your logic } receive() external payable { // 5356 gas // your logic }
The receive()
or fallback()
function can handle incoming Ether transfers directly, providing more gas-efficient way to manage deposits.
File: src/UTB.sol 108: function swapAndExecute( SwapAndExecuteInstructions calldata instructions, FeeStructure calldata fees, bytes calldata signature ) public payable retrieveAndCollectFees(fees, abi.encode(instructions, fees), signature) 259: function bridgeAndExecute( BridgeInstructions calldata instructions, FeeStructure calldata fees, bytes calldata signature ) public payable retrieveAndCollectFees(fees, abi.encode(instructions, fees), signature) returns (bytes memory)
File: src/UTBExecutor.sol 19: function execute( address target, address paymentOperator, bytes memory payload, address token, uint amount, address payable refund ) public payable onlyOwner
File: src/UTBFeeCollector.sol 44: function collectFees( FeeStructure calldata fees, bytes memory packedInfo, bytes memory signature ) public payable onlyUtb
File: src/bridge_adapters/DecentBridgeAdapter.sol 81: function bridge( uint256 amt2Bridge, SwapInstructions memory postBridge, uint256 dstChainId, address target, address paymentOperator, bytes memory payload, bytes calldata additionalArgs, address payable refund ) public payable onlyUtb returns (bytes memory bridgePayload)
File: src/bridge_adapters/StargateBridgeAdapter.sol 69: function bridge( uint256 amt2Bridge, SwapInstructions memory postBridge, uint256 dstChainId, address target, address paymentOperator, bytes memory payload, bytes calldata additionalArgs, address payable refund ) public payable onlyUtb returns (bytes memory bridgePayload)
File: src/swappers/UniSwapper.sol 100: function swapNoPath( SwapParams memory swapParams, address receiver, address refund ) public payable returns (address tokenOut, uint256 amountOut) 123: function swapExactIn( SwapParams memory swapParams, // SwapParams is a struct address receiver ) public payable routerIsSet returns (uint256 amountOut) 143: function swapExactOut( SwapParams memory swapParams, address receiver, address refundAddress ) public payable routerIsSet returns (uint256 amountIn)
</details>File: lib/decent-bridge/src/DecentEthRouter.sol 197: function bridgeWithPayload( uint16 _dstChainId, address _toAddress, uint _amount, bool deliverEth, uint64 _dstGasForCall, bytes memory additionalPayload ) public payable 218: function bridge( uint16 _dstChainId, address _toAddress, uint _amount, uint64 _dstGasForCall, bool deliverEth // if false, delivers WETH ) public payable 302: function addLiquidityEth() public payable onlyEthChain userDepositing(msg.value) 322: function addLiquidityWeth( uint256 amount ) public payable userDepositing(amount)
Before version 0.8.10, the Solidity compiler would insert extra code, such as EXTCODESIZE (costing 100 gas), to check the existence of a contract for external function calls.
Newer versions, starting from 0.8.10, no longer insert these checks if the external call has a return value.
You can achieve similar behavior in earlier Solidity versions by using low-level calls like call
.
This low-level call don't check for contract existence, saving gas costs.
File: src/UTB.sol 78: swapInstructions.swapPayload = swapper.updateSwapParams( 83: IERC20(swapParams.tokenIn).transferFrom( 90: IERC20(swapParams.tokenIn).approve( 95: (tokenOut, amountOut) = swapper.swap(swapInstructions.swapPayload); 98: wrapped.withdraw(amountOut); 152: IERC20(tokenOut).approve(address(executor), amountOut); 153: executor.execute( 188: ).getBridgedAmount(amountOut, tokenOut, newPostSwapParams.tokenIn); 194: ]).updateSwapParams( 212: address bridgeToken = bridgeAdapter.getBridgeToken( 216: IERC20(bridgeToken).approve(address(bridgeAdapter), amt2Bridge); 236: IERC20(fees.feeToken).transferFrom( 241: IERC20(fees.feeToken).approve( 327: swappers[s.getId()] = swapper; 336: bridgeAdapters[b.getId()] = bridge;
78 | 83 | 90 | 95 | 98 | 152 | 153 | 188 | 194 | 212 | 216 | 236 | 241 | 327 | 336
File: src/UTBExecutor.sol 61: IERC20(token).transferFrom(msg.sender, address(this), amount); 62: IERC20(token).approve(paymentOperator, amount); 80: IERC20(token).transfer(refund, remainingBalance);
File: src/UTBFeeCollector.sol 56: IERC20(fees.feeToken).transferFrom( 71: payable(owner).transfer(amount); 73: IERC20(token).transfer(owner, amount);
File: src/bridge_adapters/DecentBridgeAdapter.sol 56: router.estimateSendAndCallFee( 57: router.MT_ETH_TRANSFER_WITH_PAYLOAD(), 109: IERC20(bridgeToken).transferFrom( 114: IERC20(bridgeToken).approve(address(router), amt2Bridge); 139: IERC20(swapParams.tokenIn).transferFrom( 145: IERC20(swapParams.tokenIn).approve(utb, swapParams.amountIn); 147: IUTB(utb).receiveFromBridge(
56 | 57 | 109 | 114 | 139 | 145 | 147
File: src/bridge_adapters/StargateBridgeAdapter.sol 88: IERC20(bridgeToken).transferFrom(msg.sender, address(this), amt2Bridge); 89: IERC20(bridgeToken).approve(address(router), amt2Bridge); 207: IERC20(swapParams.tokenIn).approve(utb, swapParams.amountIn); 209: IUTB(utb).receiveFromBridge(
File: src/swappers/UniSwapper.sol 44: IERC20(token).transfer(user, amount); 55: IERC20(token).transfer(recipient, amount); 83: IERC20(swapParams.tokenIn).transferFrom( 130: .ExactInputParams({ 137: IERC20(swapParams.tokenIn).approve(uniswap_router, swapParams.amountIn); 138: amountOut = IV3SwapRouter(uniswap_router).exactInput(params); 150: .ExactOutputParams({ 158: IERC20(swapParams.tokenIn).approve(uniswap_router, swapParams.amountIn); 159: amountIn = IV3SwapRouter(uniswap_router).exactOutput(params);
44 | 55 | 83 | 130 | 137 | 138 | 150 | 158 | 159
</details>File: lib/decent-bridge/src/DecentBridgeExecutor.sol 31: weth.approve(target, amount); 36: weth.transfer(from, amount); 44: weth.transfer(from, remainingAfterCall); 60: weth.withdraw(amount); 63: payable(from).transfer(amount); 75: weth.transferFrom(msg.sender, address(this), amount);
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadUsage of uints/ints smaller than 32 bytes (256 bits) incurs overhead. The Ethereum Virtual Machine (EVM) operates on 32 bytes at a time. Therefore, if an element is smaller than 32 bytes, the EVM must use more operations to reduce the size of the element from 32 bytes to the desired size.
Operations involving smaller size uints/ints cost extra gas due to the compiler having to clear the higher bits of the memory word before operating on the small size integer. This also includes the associated stack operations of doing so.
It's recommended to use larger sizes and downcast where needed to optimize for gas efficiency.
<details> <summary><i>31 issue instances in 6 files:</i></summary>File: src/UTB.sol 21: mapping(uint8 => address) public swappers; 22: mapping(uint8 => address) public bridgeAdapters;
File: src/bridge_adapters/DecentBridgeAdapter.sol 13: uint8 public constant BRIDGE_ID = 0; 17: mapping(uint16 => uint256) chainIdLookup; 30: function getId() public pure returns (uint8) { 96: uint64 dstGas = abi.decode(additionalArgs, (uint64));
File: src/bridge_adapters/StargateBridgeAdapter.sol 22: uint8 public constant BRIDGE_ID = 1; 23: uint8 public constant SG_FEE_BPS = 6; 27: mapping(uint16 => uint256) chainIdLookup; 41: function getId() public pure returns (uint8) { 126: ) private pure returns (uint16) { 136: ) private pure returns (uint120) { 146: ) private pure returns (uint120) { 184: uint16, // _srcChainid bytes memory, // _srcAddress uint256, // _nonce address, // _token uint256, // amountLD bytes memory payload ) external override onlyExecutor {
22 | 23 | 27 | 41 | 126 | 136 | 146 | 184
File: src/swappers/SwapParams.sol 5: uint8 constant EXACT_IN = 0; 6: uint8 constant EXACT_OUT = 1;
File: src/swappers/UniSwapper.sol 16: uint8 public constant SWAPPER_ID = 0; 28: function getId() public pure returns (uint8) {
File: lib/decent-bridge/src/DecentEthRouter.sol 17: uint8 public constant MT_ETH_TRANSFER = 0; 18: uint8 public constant MT_ETH_TRANSFER_WITH_PAYLOAD = 1; 20: uint16 public constant PT_SEND_AND_CALL = 1; 24: mapping(uint16 => address) public destinationBridges; 74: uint16 _dstChainId, address _routerAddress ) public onlyOwner { 81: uint8 msgType, address _toAddress, uint16 _dstChainId, uint64 _dstGasForCall, bool deliverEth, bytes memory additionalPayload ) private view returns ( bytes32 destBridge, bytes memory adapterParams, bytes memory payload ) { 114: uint8 msgType, uint16 _dstChainId, address _toAddress, uint _amount, uint64 _dstGasForCall, bool deliverEth, bytes memory payload ) public view returns (uint nativeFee, uint zroFee) { 149: uint8 msgType, uint16 _dstChainId, address _toAddress, uint _amount, uint64 _dstGasForCall, bytes memory additionalPayload, bool deliverEth ) internal { 198: uint16 _dstChainId, address _toAddress, uint _amount, bool deliverEth, uint64 _dstGasForCall, bytes memory additionalPayload ) public payable { 219: uint16 _dstChainId, address _toAddress, uint _amount, uint64 _dstGasForCall, bool deliverEth // if false, delivers WETH ) public payable { 238: uint16 _srcChainId, bytes calldata, uint64, bytes32, uint _amount, bytes memory _payload ) external override onlyLzApp { 245: (uint8 msgType, address _from, address _to, bool deliverEth) = abi .decode(_payload, (uint8, address, address, bool)); 253: (uint8, address, address, bool, bytes) );
17 | 18 | 20 | 24 | 74 | 81 | 114 | 149 | 198 | 219 | 238 | 245 | 253
</details>#0 - raymondfam
2024-01-26T16:50:12Z
All findings except G-10 are generically known to complement the bot report.
#1 - c4-pre-sort
2024-01-26T16:50:17Z
raymondfam marked the issue as sufficient quality report
#2 - alex-ppg
2024-02-04T17:56:40Z
No penalization was performed, the following is noted for the Warden:
#3 - c4-judge
2024-02-04T17:56:43Z
alex-ppg marked the issue as grade-a