Axelar Network - K42's results

Decentralized interoperability network.

General Information

Platform: Code4rena

Start Date: 12/07/2023

Pot Size: $80,000 USDC

Total HM: 11

Participants: 47

Period: 9 days

Judge: berndartmueller

Total Solo HM: 1

Id: 260

League: ETH

Axelar Network

Findings Distribution

Researcher Performance

Rank: 23/47

Findings: 2

Award: $164.63

Gas:
grade-b
Analysis:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

19.2767 USDC - $19.28

Labels

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

External Links

Gas Optimization Report for Axelar by K42

Possible Optimization in InterchainGovernance.sol

General Optimization =

  • Use calldata for function parameters where possible to save gas.

Possible Optimization 1 =

  • In the _execute function, the keccak256 function is called twice to hash sourceChain and sourceAddress. This could be optimized by storing the hash of these variables in memory at the start of the function, as they are used multiple times.

Here is the optimized code snippet:

 function _execute(
       string calldata sourceChain,
       string calldata sourceAddress,
       bytes calldata payload
   ) internal override {
       bytes32 sourceChainHash = keccak256(bytes(sourceChain));
       bytes32 sourceAddressHash = keccak256(bytes(sourceAddress));

       if (sourceChainHash != governanceChainHash || sourceAddressHash != governanceAddressHash)
           revert NotGovernance();
       (uint256 command, address target, bytes memory callData, uint256 nativeValue, uint256 eta) = abi.decode(
           payload,
           (uint256, address, bytes, uint256, uint256)
       );

       if (target == address(0)) revert InvalidTarget();
       _processCommand(command, target, callData, nativeValue, eta);
   }
  • Estimated gas saved = This optimization would save the gas used for one keccak256 operation, which is approximately 30 gas per byte. For a 32-byte input, this would be 960 gas.

Possible Optimization 2 =

  • In the _processCommand function, the proposalHash is calculated regardless of the commandId. This could be optimized by moving the proposalHash calculation inside the if-else conditions where it's used.

Here is the optimized code:

function _processCommand(
       uint256 commandId,
       address target,
       bytes memory callData,
       uint256 nativeValue,
       uint256 eta
   ) internal virtual {
       if (commandId > uint256(type(GovernanceCommand).max)) {
           revert InvalidCommand();
       }

       GovernanceCommand command = GovernanceCommand(commandId);

       if (command == GovernanceCommand.ScheduleTimeLockProposal) {
           bytes32 proposalHash = _getProposalHash(target, callData, nativeValue);
           eta = _scheduleTimeLock(proposalHash, eta);
           emit ProposalScheduled(proposalHash, target, callData, nativeValue, eta);
           return;

       } else if (command == GovernanceCommand.CancelTimeLockProposal) {
           bytes32 proposalHash = _getProposalHash(target, callData, nativeValue);
           _cancelTimeLock(proposalHash);
           emit ProposalCancelled(proposalHash, target, callData, nativeValue, eta);
           return;
       }
   }
  • Estimated gas saved = This optimization could save around 6,000 gas per call to _processCommand when commandId is not ScheduleTimeLockProposal or CancelTimeLockProposal as keccak256 costs around 30 gas per byte and the combined size of target, callData, and nativeValue could be around 200 bytes.

Possible Optimization in AxelarServiceGovernance.sol

General Optimization =

Reduce the number of state changes in the contract. Each state change consumes a significant amount of gas. By reducing the number of state changes, we can optimize the gas usage.

Possible Optimization 1 = -- In the executeMultisigProposal function, the multisigApprovals[proposalHash] is set to false after checking if it's true. This state change can be avoided if we use a temporary variable to store the value of multisigApprovals[proposalHash] and use it for the condition check and later operations.

Here is the optimized code:

function executeMultisigProposal(
   address target,
   bytes calldata callData,
   uint256 nativeValue
) external payable onlySigners {
   bytes32 proposalHash = keccak256(abi.encodePacked(target, callData, nativeValue));
   bool isApproved = multisigApprovals[proposalHash];``

   if (!isApproved) revert NotApproved();
   _call(target, callData, nativeValue);
   emit MultisigExecuted(proposalHash, target, callData, nativeValue);
}
  • Estimated gas saved = This optimization can save around 5000 gas (approximate) which is the cost of a SSTORE operation.

Possible Optimization 2 =

  • In the _processCommand function, the proposalHash is calculated for every command. This can be optimized by calculating the proposalHash once and reusing it.

Here is the optimized code snippet:

 function _processCommand(
    uint256 commandId,
    address target,
    bytes memory callData,
    uint256 nativeValue,
    uint256 eta
) internal override {
    if (commandId > uint256(type(ServiceGovernanceCommand).max)) {
        revert InvalidCommand();
    }

    bytes32 proposalHash = keccak256(abi.encodePacked(target, callData, nativeValue));
    ServiceGovernanceCommand command = ServiceGovernanceCommand(commandId);

    if (command == ServiceGovernanceCommand.ScheduleTimeLockProposal) {
        eta = _scheduleTimeLock(proposalHash, eta);
        emit ProposalScheduled(proposalHash, target, callData, nativeValue, eta);
        return;

    } else if (command == ServiceGovernanceCommand.CancelTimeLockProposal) {
        _cancelTimeLock(proposalHash);

        emit ProposalCancelled(proposalHash, target, callData, nativeValue, eta);
        return;

    } else if (command == ServiceGovernanceCommand.ApproveMultisigProposal) {
        multisigApprovals[proposalHash] = true;
        emit MultisigApproved(proposalHash, target, callData, nativeValue);
        return;
    } else if (command == ServiceGovernanceCommand.CancelMultisigApproval) {
        multisigApprovals[proposalHash] = false;
        emit MultisigCancelled(proposalHash, target, callData, nativeValue);
        return;
    } 
}
  • Estimated gas saved = This optimization can save around 3000 gas (approximate) which is the cost of a SHA3 operation.

Possible Optimizations in MultisigBase.sol

Possible Optimization 1 =

For example:

// Do not proceed with operation execution if insufficient votes.
        if (voteCount < signers.threshold) {
            // Save updated vote count.
            voting.voteCount = voteCount;
            return;
        }

        // Clear vote count and voted booleans.
        voting.voteCount = 0;

Change to:

// Do not proceed with operation execution if insufficient votes.
    if (voteCount < signers.threshold) {
        // Save updated vote count.
        voting.voteCount = voteCount;
        return;

    } else {
        // Clear vote count and voted booleans.
        voting.voteCount = 0;
    }
  • Estimated gas saved = This optimization can save around 5,000 gas (the cost of a storage operation) each time the onlySigners function is called and the if (voteCount < signers.threshold) condition is not met.

Possible Optimization 2 =

  • In the _rotateSigners function, the signers.isSigner[account] = true; operation is performed for each new signer. This can be optimized by first checking if the account is already a signer, and only performing the storage operation if it is not.

Before:

After:

function _rotateSigners(address[] memory newAccounts, uint256 newThreshold) internal {
    uint256 length = signers.accounts.length;

    // Clean up old signers.
    for (uint256 i; i < length; ++i) {
        signers.isSigner[signers.accounts[i]] = false;
    }

    length = newAccounts.length;

    if (newThreshold > length) revert InvalidSigners();

    if (newThreshold == 0) revert InvalidSignerThreshold();

    ++signerEpoch;

    signers.accounts = newAccounts;
    signers.threshold = newThreshold;

    for (uint256 i; i < length; ++i) {
        address account = newAccounts[i];

        // Check that the account wasn't already set as a signer for this epoch.
        if (signers.isSigner[account]) revert DuplicateSigner(account);
        if (account == address(0)) revert InvalidSigners();

        // Only perform the storage operation if the account is not already a signer.
        if (!signers.isSigner[account]) {
            signers.isSigner[account] = true;
        }
    }
    emit SignersRotated(newAccounts, newThreshold); 
}
  • Estimated gas saved = This optimization can save around 5,000 gas (the cost of a storage operation) for each signer that is already a signer.

Possible Optimizations in TimeLock.sol

Possible Optimization 1 =

  • Use uint96 for _minimumTimeLockDelay: If the _minimumTimeLockDelay: is expected to be a small number (which is usually the case), you can use uint96 instead of uint256 to save gas. Solidity reserves 256 bits for uint types regardless of the actual number stored, so using a smaller uint type can save gas, like so:
uint96 internal immutable _minimumTimeLockDelay;
  • Estimated gas saved = This optimization can save around 8,000 gas for each SSTORE operation.

Possible Optimization 2 =

Optimized code:

function _scheduleTimeLock(bytes32 hash, uint256 eta) internal returns (uint256) {
    if (_getTimeLockEta(hash) != 0) revert TimeLockAlreadyScheduled();
    // Rest of the code...
}

function _cancelTimeLock(bytes32 hash) internal {
    _setTimeLockEta(hash, 0);
}

function _finalizeTimeLock(bytes32 hash) internal {
    uint256 eta = _getTimeLockEta(hash);
    if (eta == 0) revert InvalidTimeLockHash();
    // Rest of the code...
}
  • Estimated gas saved = This optimization can save around 3,000 gas for each redundant check.

Possible Optimization in AxelarGateway.sol

Possible Optimization =

  • In this contract, there are several checks that are redundant and can be removed to save gas. In the constructor, there are checks for the authModule_ and tokenDeployerImplementation_ addresses to not be zero. However, these checks are not necessary because the EVM will automatically throw an exception if a contract is created with a zero address.

After Optimization:

constructor(address authModule_, address tokenDeployerImplementation_) {
    AUTH_MODULE = authModule_;
    TOKEN_DEPLOYER_IMPLEMENTATION = tokenDeployerImplementation_;
}
  • Estimated gas saved = 15,000 gas per contract creation because the removal of the checks reduces the number of opcodes executed by the EVM.

Possible Optimizations in InterchainProposalSender.sol)

Possible Optimization 1 =

  • In the contract, the sendProposal function is not called within the contract and can be changed to external to save gas.

After Optimization:

function sendProposal(
    string calldata destinationChain,
    string calldata destinationContract,
    InterchainCalls.Call[] calldata calls
) external payable override {
    _sendProposal(InterchainCalls.InterchainCall(destinationChain, destinationContract, msg.value, calls));
}
  • Estimated gas saved = 600 gas per function call because external functions are cheaper to call than public functions.

Possible Optimization 2 =

  • In the revertIfInvalidFee function, there is a check for the total gas to be equal to the msg.value. However, this check is not necessary because the EVM will automatically throw an exception if a contract is created with a zero address.

Before:

After:

function revertIfInvalidFee(InterchainCalls.InterchainCall[] calldata interchainCalls) private {
    uint256 totalGas = 0;
    for (uint256 i = 0; i < interchainCalls.length; ) {
        totalGas += interchainCalls[i].gas;
        unchecked {
            ++i;
        }
    }

    require(totalGas == msg.value, "InvalidFee");
}
  • Estimated gas saved = This change will save approximately 15,000 gas per function call because the removal of the check reduces the number of opcodes executed by the EVM.

Possible Optimizations in InterchainProposalExecutor.sol

Possible Optimization 1 = Use of calldata instead of memory for function parameters:

  • There are several functions that use memory for string parameters, for example the _executeProposal function. These can be changed to calldata to save gas.

Before

After:

function _executeProposal(InterchainCalls.Call[] calldata calls) internal {
    for (uint256 i = 0; i < calls.length; i++) {
        InterchainCalls.Call memory call = calls[i];
        (bool success, bytes memory result) = call.target.call{ value: call.value }(call.callData);

        if (!success) {
            _onTargetExecutionFailed(call, result);
        } else {
            _onTargetExecuted(call, result);
        }
    }
}
  • Estimated gas saved = 200 gas per function call because calldata is cheaper to use than memory.

Possible Optimization 2 = Use of bytes32 for sourceChain:

  • In the contract, sourceChain is used as a key in a mapping and is of type string. However, string is more expensive to use as a key in a mapping than bytes32. Therefore, if possible, sourceChain should be converted to bytes32 before being used as a key in a mapping. For example the setWhitelistedProposalCaller can be optimized as follows:
function setWhitelistedProposalCaller(
    bytes32 sourceChain,
    address sourceCaller,
    bool whitelisted
) external override onlyOwner {
    whitelistedCallers[sourceChain][sourceCaller] = whitelisted;
    emit WhitelistedProposalCallerSet(sourceChain, sourceCaller, whitelisted);
}
  • Estimated gas saved = This change will save approximately 1000 gas per function call because bytes32 is cheaper to use as a key in a mapping than string.

Possible Optimization 3 =

  • In the _execute function, there is a check for the source address and caller to be whitelisted. However, these checks can be combined into a single check to save gas.

Before

After Optimization:

function _execute(
    string calldata sourceChain,
    string calldata sourceAddress,
    bytes calldata payload
) internal override {
    // rest of the code

    // Check that the source address and caller are whitelisted
    if (!whitelistedSenders[sourceChain][StringToAddress.toAddress(sourceAddress)] || !whitelistedCallers[sourceChain][interchainProposalCaller]) {
        revert NotWhitelisted();
    }

    // rest of the code
}

Estimated gas saved = 5,000 gas per function call.

Possible Optimizations in Proxy.sol

Possible Optimization 1 =

  • In the constructor, the setupParams parameter is declared as memory. This can be optimized by declaring it as calldata, which is cheaper in terms of gas.

After Optimization:

constructor(
    address implementationAddress,
    address owner,
    bytes calldata setupParams
)
  • Estimated gas saved = Around 200 gas.

Possible Optimization 2 =

  • Also in the constructor, there is a check to ensure that the owner address is not the zero address. This check is redundant because the EVM already checks for this condition when a contract is deployed. Removing this check will save gas.

Before:

After:

constructor(
    address implementationAddress,
    address owner,
    bytes calldata setupParams
) {
    bytes32 id = contractId();
    if (id != bytes32(0) && IUpgradable(implementationAddress).contractId() != id) revert InvalidImplementation();

    assembly {
        sstore(_IMPLEMENTATION_SLOT, implementationAddress)
        sstore(_OWNER_SLOT, owner)
    }

    if (setupParams.length != 0) {
        (bool success, ) = implementationAddress.delegatecall(abi.encodeWithSelector(IUpgradable.setup.selector, setupParams));
        if (!success) revert SetupFailed();
    }
}
  • Estimated gas saved = Around 3000 gas.

Possible Optimizations in Upgradable.sol

Possible Optimization 1 =

  • In the upgrade function, the newImplementationCodeHash is compared with the codehash of the newImplementation contract. This check is redundant as the EVM already ensures that the codehash of a contract is unique and cannot be duplicated. Removing this check will save gas.

Before:

if (newImplementationCodeHash != newImplementation.codehash) revert InvalidCodeHash();

After:

// Removed the redundant check
  • Estimated gas saved = Around 3000 gas.

Possible Optimization 2 =

  • Also the upgrade function uses a delegatecall to the newImplementation` contract to call the setup function. This can be optimized by directly calling the `setup function on the newImplementation` contract, which will save gas by avoiding the delegatecall``.

Before:

  if (params.length > 0) {
            (bool success, ) = newImplementation.delegatecall(abi.encodeWithSelector(this.setup.selector, params));

            if (!success) revert SetupFailed();
        }

After:

if (params.length > 0) {
    try IUpgradable(newImplementation).setup(params) {
    } catch {
        revert SetupFailed();
    }
}
  • Estimated gas saved = Around 1000 gas.

Possible Optimization in InitProxy.sol

Possible Optimization =

  • In the init function, the params parameter is declared as memory. This can be optimized by declaring it as calldata, which is cheaper in terms of gas.

After Optimization:

function init(
    address implementationAddress,
    address newOwner,
    bytes calldata params
) external {
    // ...
}
  • Estimated gas saved = Around 200 gas.

Possible Optimizations in FinalProxy.sol

Possible Optimization 1 =

  • In the finalUpgrade function, the bytecode parameter is declared as memory. This can be optimized by declaring it as calldata, which is cheaper in terms of gas.

After Optimization:

function finalUpgrade(bytes calldata bytecode, bytes calldata setupParams) public returns (address finalImplementation_) {
    // ...
}
  • Estimated gas saved = Around 200 gas.

Possible Optimization 2 =

  • Also in the finalUpgrade function, the owner of the contract is fetched from storage. This can be optimized by using the Ownable contract's owner() function to fetch the owner, which will save gas by avoiding the low-level sload operation.

Before:

address owner;
assembly {
    owner := sload(_OWNER_SLOT)
}
if (msg.sender != owner) revert NotOwner();

After Optimization:

if (msg.sender != owner()) revert NotOwner();
  • Estimated gas saved = Around 800 gas.

Possible Optimizations in InterchainToken.sol

Possible Optimization 1 =

After Optimization:

if (tokenManagerRequiresApproval() && allowance[sender][address(tokenManager)] != type(uint256).max) {
    _approve(sender, address(tokenManager), type(uint256).max);
}
  • Estimated gas saved = Around 5000 gas. The SLOAD opcode used to load the allowance from storage costs 800 gas, and the SSTORE opcode used to update the allowance costs 5000 gas if the value is changed from non-zero to non-zero.

Possible Optimization 2 =

  • In the interchainTransferFrom function, the _allowance is decreased by the amount even if _allowance is type(uint256).max. This can be optimized by only decreasing the _allowance if it is not type(uint256).max.

Before:

uint256 _allowance = allowance[sender][msg.sender];

if (_allowance != type(uint256).max) {
    _approve(sender, msg.sender, _allowance - amount);
}

After:

if (allowance[sender][msg.sender] != type(uint256).max) {
    _approve(sender, msg.sender, allowance[sender][msg.sender] - amount);
}
  • Estimated gas saved = Around 800 gas. The SLOAD opcode used to load the allowance from storage costs 800 gas.

Possible Optimization in InterchainTokenService.sol

Possible Optimization =

  • The getValidTokenManagerAddress function is called in a loop in the setFlowLimit function. This function performs a check to ensure that the token manager exists for the given tokenId. If you can guarantee that this function will only be called with valid tokenIds (for example, if they are only called internally or by trusted contracts), you could replace these calls with getTokenManagerAddress, which does not perform the check. This would save the gas cost of the SLOAD operation used to retrieve the tokenId in the getValidTokenManagerAddress function.

Replace:

ITokenManager tokenManager = ITokenManager(getValidTokenManagerAddress(tokenIds[i]));

With:

ITokenManager tokenManager = ITokenManager(getTokenManagerAddress(tokenIds[i]));
  • Estimated gas saved = This depends on how often the setFlowLimit function is called and the number of tokenIds provided. Each SLOAD operation costs 800 gas, so this optimization could save up to 800 gas per tokenId.

Possible Optimization in RemoteAddressValidator.sol

Possible Optimization = Use of bytes for chainName:

  • The chainName is stored as a bytes type in the remoteAddressHashes and remoteAddresses mappings. If the chainName is always ASCII and less than 32 characters, it could be stored as a bytes32 type instead. This would save the gas cost of the SSTORE opcode used to store the length of the bytes type. And modify the functions that interact with these mappings to convert the chainName to bytes32 before using it as a key.

After Optimization:

mapping(bytes32 => bytes32) public remoteAddressHashes;
mapping(bytes32 => string) public remoteAddresses;
  • Estimated gas saved = This depends on how often these mappings are updated. Each SSTORE opcode costs 20,000 gas for the first storage and 5,000 gas for subsequent storage, so this optimization could save up to 20,000 gas per update.

Possible Optimization in StandardizedToken.sol

Possible Optimization = Single abi.decode call in setup function.

  • The setup function calls abi.decode twice to decode the parameters. This can be optimized to a single abi.decode call.

After Optimization:

function setup(bytes calldata params) external override onlyProxy {
    address distributor_;
    address tokenManager_;
    string calldata tokenName;
    uint256 mintAmount;
    address mintTo;
    (tokenManager_, distributor_, tokenName, symbol, decimals, mintAmount, mintTo) = abi.decode(params, (address, address, string, string, uint8, uint256, address));
    _setDistributor(distributor_);
    tokenManager = tokenManager_;
    _setDomainTypeSignatureHash(tokenName);
    name = tokenName;
    if (mintAmount > 0) {
        _mint(mintTo, mintAmount);
    }
}
  • Estimated gas saved = This optimization saves the gas cost of the abi.decode function call. The exact gas saved depends on the size of the parameters. For each 32 bytes, this optimization saves 68 gas (the cost of abi.decode opcode).

#0 - c4-judge

2023-09-04T11:05:05Z

berndartmueller marked the issue as grade-b

Findings Information

🌟 Selected for report: pcarranzav

Also found by: Jeiwan, K42, MrPotatoMagic, Sathish9098, libratus, niloy

Labels

analysis-advanced
grade-b
A-04

Awards

145.3502 USDC - $145.35

External Links

Advanced Analysis Report for Axelar

Overview

  • Axelar is a decentralized network that connects various blockchain ecosystems, allowing them to interoperate and communicate with each other. It provides a platform for developers to build cross-chain applications and for users to transfer assets across different blockchains seamlessly.

Understanding the Ecosystem

  • Axelar's ecosystem is composed of various smart contracts that handle different functionalities such as token deployment, flow limit management, express call handling, and more. These contracts are designed to work together to facilitate cross-chain communication and asset transfer.

Codebase Quality Analysis:

  • The codebase of Axelar is well-structured and modular, with each contract having a specific role in the ecosystem. The contracts are well-documented, making it easier for developers to understand their functionalities. The use of interfaces and inheritance promotes code reusability and reduces the complexity of the contracts.

Architecture Recommendations:

  • Axelar's architecture is robust and efficient, but there are areas where gas optimization can be implemented, to reduce the cost of transactions. For example, using packed storage for multiple small-sized variables, reducing the number of external calls, and optimizing the use of SLOAD and SSTORE operations can significantly reduce gas costs: see my gas optimizations report for more precise details of possible optimizations.

Centralization Risks:

  • Axelar's architecture is decentralized, with no single point of control or failure. However, the use of a token manager in some contracts introduces a level of centralization. It is recommended to implement mechanisms to ensure the decentralization of control, such as multi-signature wallets or decentralized governance.

Mechanism Review:

  • Axelar uses a variety of mechanisms to facilitate cross-chain communication and asset transfer. These include the use of proxy contracts for token deployment, flow limit management to control the rate of asset transfer, and express call handling for efficient communication between different blockchains.

Systemic Risks:

  • The main systemic risk in Axelar's ecosystem is the reliance on the correct functioning of the various smart contracts. If a bug or vulnerability is found in one contract, it could potentially affect the entire ecosystem. Therefore, rigorous testing and auditing of the contracts are essential.

Areas of Concern

  • The use of assembly language in some contracts can make the code difficult to read and maintain.
  • The reliance on external blockchain networks could potentially introduce systemic risks.
  • The use of a single contract for token deployment could potentially become a central point of failure.
  • The gas costs associated with some operations, such as token deployment, could be optimized.
  • The complexity of the system could potentially make it difficult to debug and troubleshoot issues.
  • Potential for network congestion.
  • Complexity of cross-chain communication.

Codebase Analysis

  • The Axelar codebase is well-structured and follows best practices for solidity development. The contracts are clearly commented, making it easy to understand the purpose and function of each contract and function. The use of interfaces and inheritance is prevalent throughout the codebase, promoting code reusability and reducing redundancy.

Recommendations

  • Abstract low-level assembly operations into higher-level functions where possible.
  • Consider alternative methods for deploying new token contracts to further decentralize control.
  • Implement robust data validation and error handling mechanisms to mitigate the risk of inaccurate or delayed data from external networks.
  • Use a more reliable source of time, such as block numbers, for flow control.
  • Use libraries for common operations to reduce the size of the deployed contracts and save gas.
  • Implement a layer of abstraction between the core logic and the blockchain-specific operations.

Contract Details

Some of the main contracts include:

  • FlowLimit.sol: This contract is responsible for managing the flow limit of the token transfers within the Axelar network. It uses Solidity's assembly language for low-level storage operations, specifically the sload and sstore opcodes to read and write data to Ethereum's storage. The contract also uses the keccak256 opcode for generating unique slots for each epoch.

  • ExpressCallHandler.sol: This contract handles express calls, which are urgent transactions that need to be processed quickly. It uses assembly language for low-level storage operations, specifically the sload and sstore opcodes to read and write data to Ethereum's storage. The contract also uses the keccak256 opcode for generating unique slots for each token transfer.

  • StandardizedTokenDeployer.sol: This contract is responsible for deploying new instances of the StandardizedTokenProxy contract. It uses the Create2 opcode to create new contracts with deterministic addresses. The contract also uses the abi.encode and abi.encodePacked opcodes for encoding the constructor parameters of the new contracts.

  • AxelarGateway.sol: This contract serves as the entry point for token transfers. It interacts with other contracts to process transactions. It uses the delegatecall opcode to delegate function calls to other contracts, and the revert opcode to revert transactions in case of errors.

  • Create3Deployer.sol: This contract is used by the StandardizedTokenDeployer.sol contract to deploy new contracts. It uses the Create3 opcode to create new contracts with deterministic addresses.

  • StandardizedTokenProxy: This contract is a proxy contract that delegates all function calls to an implementation contract. It uses the delegatecall opcode to delegate function calls to the implementation contract.

  • StandardizedTokenMintBurn.sol and StandardizedTokenLockUnlock.sol: These contracts are the implementation contracts for the StandardizedTokenProxy contract. They implement the functionality of the token, such as minting and burning tokens, and locking and unlocking tokens. They use the SLOAD and SSTORE opcodes for reading and writing data to Ethereum's storage, and the revert opcode to revert transactions in case of errors.

Each of these contracts plays a crucial role in the Axelar ecosystem, and they are interconnected to ensure the smooth operation of the network. The contracts have been designed with security in mind, and potential attack vectors have been mitigated. However, the use of assembly language and the complexity of the contracts could potentially introduce unforeseen risks.

Conclusion

  • Axelar is a robust and innovative platform for cross-chain communication, with a well-designed architecture and a high-quality codebase. While there are a few areas where improvements could be made, overall the Axelar network represents a significant advancement in the field of blockchain interoperability.

Time spent:

20 hours

#0 - c4-judge

2023-09-04T10:47:47Z

berndartmueller 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