Ajna Protocol - j4ld1na's results

A peer to peer, oracleless, permissionless lending protocol with no governance, accepting both fungible and non fungible tokens as collateral.

General Information

Platform: Code4rena

Start Date: 03/05/2023

Pot Size: $60,500 USDC

Total HM: 25

Participants: 114

Period: 8 days

Judge: Picodes

Total Solo HM: 6

Id: 234

League: ETH

Ajna Protocol

Findings Distribution

Researcher Performance

Rank: 102/114

Findings: 1

Award: $22.28

Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

22.2767 USDC - $22.28

Labels

bug
G (Gas Optimization)
grade-b
G-15

External Links

IssuesInstances
G-01Default value initialization.24
G-02Do not calculate constants.6
G-03Using calldata instead of memory for read-only arguments in external functions saves gas.6
G-04abi.encode() is less efficient than abi.encodePacked().7
G-05Use assembly to check for address(0).1
G-06Revert as early as possible.1
G-07When possible, use assembly instead of unchecked{++i}.1
G-08Assembly for If Statements. Save Gas.1

[01] Default value initialization.

If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type). Explicitly initializing it with its default value is an anti-pattern and wastes gas.

There are 24 instances:

ajna-core/src/PositionManager.sol#L181

for (uint256 i = 0; i < indexesLength; ) {

ajna-core/src/PositionManager.sol#L364

for (uint256 i = 0; i < indexesLength; ) {

ajna-core/src/PositionManager.sol#L474

uint256 filteredIndexesLength = 0;

ajna-core/src/PositionManager.sol#L476

for (uint256 i = 0; i < indexesLength; ) {

ajna-core/src/RewardsManager.sol#L163

for (uint256 i = 0; i < fromBucketLength; ) {

ajna-core/src/RewardsManager.sol#L229

for (uint256 i = 0; i < positionIndexes.length; ) {

ajna-core/src/RewardsManager.sol#L290

for (uint256 i = 0; i < positionIndexes.length; ) {

ajna-core/src/RewardsManager.sol#L440

for (uint256 i = 0; i < positionIndexes_.length; ) {

ajna-core/src/RewardsManager.sol#L680

for (uint256 i = 0; i < indexes_.length; ) {

ajna-core/src/RewardsManager.sol#L704

for (uint256 i = 0; i < indexes_.length; ) {

ajna-grants/src/grants/base/Funding.sol#L62

for (uint256 i = 0; i < targets_.length; ++i) {

ajna-grants/src/grants/base/Funding.sol#L112

for (uint256 i = 0; i < targets_.length;) {

ajna-grants/src/grants/base/StandardFunding.sol#L63

uint24 internal _currentDistributionId = 0;

ajna-grants/src/grants/base/StandardFunding.sol#L208

for (uint i = 0; i < numFundedProposals; ) {

ajna-grants/src/grants/base/StandardFunding.sol#L324

for (uint i = 0; i < numProposalsInSlate; ) {

ajna-grants/src/grants/base/StandardFunding.sol#L431

uint256 totalTokensRequested = 0;

ajna-grants/src/grants/base/StandardFunding.sol#L434

for (uint i = 0; i < numProposalsInSlate_; ) {

ajna-grants/src/grants/base/StandardFunding.sol#L468

for (uint i = 0; i < numProposals; ) {

ajna-grants/src/grants/base/StandardFunding.sol#L491

for (uint i = 0; i < proposalIdSubset_.length;) {

ajna-grants/src/grants/base/StandardFunding.sol#L549

for (uint256 i = 0; i < numVotesCast; ) {

ajna-grants/src/grants/base/StandardFunding.sol#L582

for (uint256 i = 0; i < numVotesCast; ) {

ajna-grants/src/grants/base/StandardFunding.sol#L770

for (int256 i = 0; i < arrayLength;) {

ajna-grants/src/grants/base/StandardFunding.sol#L797

for (int256 i = 0; i < numVotesCast; ) {

ajna-grants/src/grants/base/StandardFunding.sol#L848

for (uint256 i = 0; i < numVotesCast; ) {

Recommended Mitigation Steps:

  • Remove explicit initialization for default values.

[02] - Do not calculate constants.

Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas.

There are 6 instances:

ajna-core/src/RewardsManager.sol#L46

uint256 internal constant REWARD_CAP = 0.8 * 1e18;

ajna-core/src/RewardsManager.sol#L50

uint256 internal constant UPDATE_CAP = 0.1 * 1e18;

ajna-core/src/RewardsManager.sol#L55

uint256 internal constant REWARD_FACTOR = 0.5 * 1e18;

ajna-core/src/RewardsManager.sol#L59

uint256 internal constant UPDATE_CLAIM_REWARD = 0.05 * 1e18;

ajna-grants/src/grants/base/StandardFunding.sol#L27

uint256 internal constant GLOBAL_BUDGET_CONSTRAINT = 0.03 * 1e18;

ajna-grants/src/grants/libraries/Maths.sol#L6

uint256 public constant WAD = 10**18;

Recommended Mitigation Steps:

  • Do not calculate constants.

[03] - Using calldata instead of memory for read-only arguments in external functions saves gas.

When a function with a memory array is called externally, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length). Using calldata directly, obliviates the need for such a loop in the contract code and runtime execution. Structs have the same overhead as an array of length one.

There are 6 instances:

ajna-grants/src/grants/GrantFund.sol#L23-L25

    function hashProposal(
        address[] memory targets_,
        uint256[] memory values_,
        bytes[] memory calldatas_,
        bytes32 descriptionHash_
    ) external pure override returns (uint256 proposalId_) {

ajna-core/src/RewardsManager.sol#L137-L138

    function moveStakedLiquidity(
        uint256 tokenId_,
        uint256[] memory fromBuckets_,
        uint256[] memory toBuckets_,
        uint256 expiry_
    ) external nonReentrant override {

ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L57-L59

    function executeExtraordinary(
        address[] memory targets_,
        uint256[] memory values_,
        bytes[] memory calldatas_,
        bytes32 descriptionHash_
    ) external nonReentrant override returns (uint256 proposalId_) {

ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L87-L89

    function proposeExtraordinary(
        uint256 endBlock_,
        address[] memory targets_,
        uint256[] memory values_,
        bytes[] memory calldatas_,
        string memory description_) external override returns (uint256 proposalId_) {

ajna-grants/src/grants/base/StandardFunding.sol#L344-L346

    function executeStandard(
        address[] memory targets_,
        uint256[] memory values_,
        bytes[] memory calldatas_,
        bytes32 descriptionHash_
    ) external nonReentrant override returns (uint256 proposalId_) {

ajna-grants/src/grants/base/StandardFunding.sol#L367-L370

    function proposeStandard(
        address[] memory targets_,
        uint256[] memory values_,
        bytes[] memory calldatas_,
        string memory description_
    ) external override returns (uint256 proposalId_) {

Recommended Mitigation Steps:

  • When arguments are read-only on external functions, the data location should be calldata.

[04] - abi.encode() is less efficient than abi.encodePacked().

abi.encode will apply ABI encoding rules. Therefore all elementary types are padded to 32 bytes and dynamic arrays include their length. Therefore it is possible to also decode this data again (with abi.decode) when the type are known.

abi.encodePacked will only use the minimal required memory to encode the data. E.g. an address will only use 20 bytes and for dynamic arrays only the elements will be stored without length. For more info see the Solidity docs for packed mode.

There are 7 instances:

ajna-grants/src/grants/base/Funding.sol#L158

proposalId_ = uint256(
    keccak256(abi.encode(targets_, values_, calldatas_, descriptionHash_))
);

ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L62

proposalId_ = _hashProposal(
    targets_,
    values_,
    calldatas_,
    keccak256(
        abi.encode(DESCRIPTION_PREFIX_HASH_EXTRAORDINARY, descriptionHash_)
    )
);

ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L92

proposalId_ = _hashProposal(
    targets_,
    values_,
    calldatas_,
    keccak256(
        abi.encode(
            DESCRIPTION_PREFIX_HASH_EXTRAORDINARY,
            keccak256(bytes(description_))
        )
    )
);

ajna-grants/src/grants/base/StandardFunding.sol#L314

bytes32 newSlateHash = keccak256(abi.encode(proposalIds_));

ajna-grants/src/grants/base/StandardFunding.sol#L349

proposalId_ = _hashProposal(
    targets_,
    values_,
    calldatas_,
    keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_STANDARD, descriptionHash_))
);

ajna-grants/src/grants/base/StandardFunding.sol#L372

proposalId_ = _hashProposal(
    targets_,
    values_,
    calldatas_,
    keccak256(
        abi.encode(
            DESCRIPTION_PREFIX_HASH_STANDARD,
            keccak256(bytes(description_))
        )
    )
);

ajna-grants/src/grants/base/StandardFunding.sol#L983

return keccak256(abi.encode(proposalIds_));

Recommended Mitigation Steps:

  • Recommended change abi.encode() for abi.encodePacked() .

[05] - Use assembly to check for address(0).

There an instance:

ajna-core/src/RewardsManager.sol#L96

if (ajnaToken_ == address(0)) revert DeployWithZeroAddress();

Recommended Mitigation Steps:

    assembly {
           if iszero(ajnaToken_) {
               mstore(0x00, "zero address")
               revert(0x00, 0x20)
           }
       }
    ```

[06] - Revert as early as possible.

You pay gas for everything before the require() in the revert case.

The an instance:

ajna-core/src/PositionManager.sol#L233

if (!_isAjnaPool(params_.pool, params_.poolSubsetHash)) revert NotAjnaPool();

Recommended Mitigation Steps:

  • Change line 233 to line 230. Before tokenId_ = _nextId++;.

[07] - When possible, use assembly instead of unchecked{++i}.

You can also use unchecked{++i;} for even more gas savings but this will not check to see if i overflows. For best gas savings, use inline assembly, however this limits the functionality you can achieve.

Example:

//loop with unchecked{++i}
function uncheckedPlusPlusI() public pure {
    uint256 j = 0;
    for (uint256 i; i < 10; ) {
        j++;
        unchecked {
            ++i;
        }
    }
}

Gas: 1329

//loop with inline assembly
function inlineAssemblyLoop() public pure {
    assembly {
        let j := 0
        for {
            let i := 0
        } lt(i, 10) {
            i := add(i, 0x01)
        } {
            j := add(j, 0x01)
        }
    }
}

Gas: 709

There an instance:

ajna-core/src/RewardsManager.sol#L293

        for (uint256 i = 0; i < positionIndexes.length; ) {
            delete stakeInfo.snapshot[positionIndexes[i]]; // reset BucketState struct for current position

            unchecked { ++i; }
        }

Recommended Mitigations Steps:

  • See example

[06] Assembly for If Statements. Save Gas.

ajna-core/src/RewardsManager.sol#L815

Example:

assembly {
    if gt(x, 2) { x := mul(2, x) }
}

There an instances:

if (rewardsEarned_ > ajnaBalance) rewardsEarned_ = ajnaBalance;

Recommended Mitigations Steps:

  • See example

#0 - c4-judge

2023-05-17T10:51:45Z

Picodes marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter