📝 Common Solidity Interview Questions 📜

truongpx396

Truong Phung

Posted on November 14, 2024

📝 Common Solidity Interview Questions 📜

Here are 45 common interview questions that focus on Solidity, the popular programming language used for writing smart contracts on Ethereum:

A. Basic Concepts

1. What is Solidity, and how does it differ from other programming languages?

  • Solidity is a high-level, statically-typed programming language specifically designed for implementing smart contracts on Ethereum and other blockchains. It’s influenced by JavaScript, Python, and C++.

2. What are the key components of a smart contract in Solidity?

  • The key components include state variables, functions, modifiers, events, and structs.

3. What is the difference between public, private, internal, and external visibility in Solidity?

  • Public: Accessible both inside and outside the contract.
  • Private: Only accessible within the contract that defines it.
  • Internal: Accessible within the contract and derived contracts.
  • External: Only callable from outside the contract; more gas-efficient than public when called externally.

4. How do you define a state variable in Solidity?

  • State variables store data permanently on the blockchain.
    Example:

    uint public balance;
    

5. What are events in Solidity, and why are they important?

  • Events are logs on the Ethereum blockchain that notify external applications about contract activity. They are useful for triggering actions in off-chain applications like dApps.

6. What is the difference between view and pure functions in Solidity?

  • View: Read-only functions that don’t modify the state.
  • Pure: Functions that neither modify nor read the state; they only operate on input arguments.

7. What is a fallback function in Solidity, and when is it triggered?

  • The fallback function is executed when a contract receives Ether but no data (or an unknown function call). It is used to handle such cases gracefully.

    fallback() external payable { }
    

8. How does inheritance work in Solidity?

  • Solidity supports single and multiple inheritance, allowing contracts to inherit properties and functions from parent contracts. Derived contracts can override parent functions using the override keyword.

9. What is a constructor in Solidity, and how is it used?

  • A constructor is a special function that initializes a contract when it is deployed. It is only executed once during the contract's creation.

10. What are the differences between storage and memory keywords in Solidity?

  • Storage: Persistent, stored on-chain. Variables declared in storage are written permanently to the blockchain.
  • Memory: Temporary, used during function execution. It is not stored on-chain and is cheaper in terms of gas.

B. Advanced Concepts

11. What is Gas in Solidity, and how does it impact smart contract execution?

  • Gas is a unit that measures the amount of computational effort required to execute operations in Ethereum. It prevents infinite loops and incentivizes efficient coding.

12. What is the purpose of require, assert, and revert statements in Solidity?

  • require: Validates inputs and conditions. If the condition fails, the transaction is reverted.
  • assert: Used to check for internal errors and conditions that should never happen. It’s more expensive in terms of gas.
  • revert: Used to manually stop execution and revert the contract state.

13. What is the difference between delegatecall and call?

  • call: Calls a function of another contract, changing the context to that of the called contract.
  • delegatecall: Calls a function but keeps the context of the calling contract. It’s used in proxy patterns for contract upgrades.

14. What are modifiers in Solidity, and how do you use them?

  • Modifiers are used to change the behavior of functions. They allow for preconditions before the execution of a function.
  • Example:

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }
    

15. What is the purpose of the selfdestruct function in Solidity?

  • selfdestruct is used to delete a contract from the blockchain, sending any remaining Ether to a specified address. It’s typically used for contract upgrades or when a contract is no longer needed.

16. How does Solidity handle function overloading?

  • Solidity allows multiple functions with the same name but different parameters. The compiler differentiates them based on the parameter types and count.

17. How would you implement a contract upgrade in Solidity?

  • Contract upgrades can be implemented using proxy patterns, where the proxy contract delegates calls to the implementation contract. This allows changes to the logic without altering the contract's storage.

18. What are libraries in Solidity, and how do they differ from contracts?

Libraries are similar to contracts but they cannot store state and cannot receive Ether. They are reusable and their code can be called from contracts without redeploying.

19. What are interfaces in Solidity, and why are they used?

  • Interfaces define a contract’s external functions without including their implementation. They allow for interoperability between contracts. Example:

    interface Token {
        function transfer(address _to, uint256 _value) external;
    }
    

20. What are structs in Solidity, and how are they used?

  • Structs are custom data types that group multiple variables together. Example:

    struct Person {
        string name;
        uint age;
    }
    

C. Security Considerations

21. What is a re-entrancy attack, and how can it be mitigated in Solidity?

  • A re-entrancy attack occurs when a malicious contract calls a vulnerable contract multiple times before the initial execution is completed, often draining funds. Mitigation techniques include:
    • Using the Checks-Effects-Interactions pattern.
    • Using the reentrancyGuard modifier from libraries like OpenZeppelin.

22. What is the Checks-Effects-Interactions pattern, and why is it important?

  • It is a Solidity best practice to first check conditions (Checks), then update the contract’s state (Effects), and finally interact with other contracts (Interactions). This minimizes vulnerabilities such as re-entrancy.

23. What is a front-running attack in Ethereum, and how can it be prevented in Solidity?

  • Front-running occurs when someone exploits the visibility of pending transactions by submitting their own transaction with a higher gas fee to get processed first. Mitigation strategies include using commit-reveal schemes or adjusting transaction gas fees dynamically.

24. What are integer overflows/underflows, and how can you prevent them?

  • Integer overflow happens when an arithmetic operation exceeds the storage limit of a variable. This can be prevented by using libraries like OpenZeppelin’s SafeMath, which checks for overflows and underflows (Solidity 0.8.0 upwards natively supports this).

25. How can you protect smart contracts against Denial of Service (DoS) attacks?

  • To prevent DoS attacks, avoid gas-heavy loops, use pull over push patterns for funds withdrawal, and limit external calls within functions.

26. What are some best practices for ensuring the security of Solidity smart contracts?

  • Use known libraries (e.g., OpenZeppelin).
  • Implement proper access control (e.g., owner checks).
  • Avoid state changes before external calls.
  • Limit the complexity of contracts.
  • Conduct thorough audits and testing (including fuzzing).

D. Tokens, Standards, and Best Practices

27. What are ERC-20 tokens, and what are the key functions in an ERC-20 contract?

  • ERC-20 is a standard for fungible tokens on Ethereum. Key functions include:
    • balanceOf
    • transfer
    • approve
    • transferFrom
    • allowance

28. What is the difference between ERC-20 and ERC-721?

  • ERC-20: A standard for fungible tokens, where each token is identical.
  • ERC-721: A standard for non-fungible tokens (NFTs), where each token is unique.

29. How do you test Solidity smart contracts?

  • Testing can be done using frameworks like Truffle, Hardhat, or Brownie. Contracts can be tested locally on simulated blockchains (e.g., Ganache) or on testnets before mainnet deployment. Tools like Chai or Mocha are often used for writing unit tests.

30. What is OpenZeppelin, and why is it important in Solidity development?

  • OpenZeppelin is a popular framework that provides secure, reusable libraries and contracts for Solidity. It includes implementations of standard token contracts (ERC-20, ERC-721), access control patterns, and more.

E. Other advanced ones

31. What is the CREATE2 opcode, and how does it differ from CREATE?

  • CREATE2 allows contracts to be deployed to a deterministic address based on the contract bytecode, deployer's address, and a salt value.
  • Pros:
    • Enables pre-calculation of contract addresses before deployment.
    • Useful for factory contracts, ensuring the same contract address across networks.
  • Cons:
    • Contracts cannot be deployed twice with the same bytecode and salt combination.
  • Example:

    pair = address(uint(keccak256(abi.encodePacked(
         hex'ff',
         factory,
         keccak256(abi.encodePacked(token0, token1)), hex'ced7c507bf75a9c4a42a9c14d582db9f48b2de7a90ccc86d338a41f541fe4f53' // INIT_CODE_PAIR_HASH of Pancake Factory
    ))));
    

    Ex2

    address contractAddress = address(uint160(uint256(keccak256(abi.encodePacked(
        bytes1(0xff), 
        deployer, 
        salt, 
        keccak256(bytecode)
    )))));
    

32. How does abi.encode, abi.encodePacked, abi.encodeWithSelector and abi.encodeWithSignature differ in Solidity, and when would you use each?

  • abi.encode: Encodes values into tightly packed binary form, used for general-purpose encoding.
  • abi.encodePacked: Encodes data in a packed format (no padding), useful for hash generation but can cause collisions with dynamic types.
  • abi.encodeWithSelector: Encodes the function selector (i.e., the first 4 bytes of the function's keccak256 hash) and arguments needed to call, used for low-level call or delegatecall. This method is typically used when you know the exact function you want to call and its selector.
  • abi.encodeWithSignature: Similar to abi.encodeWithSelector but encodes function call with a specific signature (i.e., the function name and its arguments). This method is typically convenient when the function signature is a string and the selector is not precomputed.
  • Use cases:
    • abi.encode for data storage or passing arguments.
    • abi.encodePacked for hash-based operations.
    • abi.encodeWithSelector for calling other contracts without ABI, it is is slightly more gas-efficient than abi.encodeWithSignature because it avoids recalculating the selector.
    • abi.encodeWithSignature for simplicity when you only have the signature string.
  • Example:
    abi.encode

     DOMAIN_SEPARATOR = keccak256(
        abi.encode(
            keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
            keccak256(bytes(name)),
            keccak256(bytes('1')),
            chainId,
            address(this)
        )
    );
    

    abi.encodePacked

     messageHash = keccak256(abi.encodePacked(...));
    

    abi.encodeWithSelector

     // This extracts the first 4 bytes of the 32-byte keccak256 hash. These first 4 bytes form the function selector for the transfer function.
     // The function selector is essentially the "address" of the function within the smart contract. When an external call is made to the contract, the EVM uses the function selector to determine which function to execute.
     bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))))
     (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
    
     // Some ERC-20 tokens don’t follow the standard perfectly. For example, some older tokens don’t return a boolean from their transfer function. 
     // Using a low-level call with this flexible checking mechanism allows the function to work with both types of tokens (those that return a boolean and those that don’t).
     require(success && (data.length == 0 || abi.decode(data, (bool))), 'Pancake: TRANSFER_FAILED');
    

    abi.encodeWithSignature

    bytes memory encoded = abi.encodeWithSignature("transfer(address,uint256)", recipient, amount);
    

33. What is the difference between keccak256 and sha256 in Solidity, and when would you use each?

  • keccak256: Ethereum’s primary hashing algorithm, based on SHA-3, used for signatures, address generation, and storing hashed data.
  • sha256: Standard SHA-2 hashing algorithm, often used for interoperability with external systems.
  • Use cases:
    • Use keccak256 for Ethereum-specific functions like signature verification.
    • Use sha256 for off-chain integrations or systems using SHA-2 standards.
  • Example:
    keccak256

     bytes32 digest = keccak256(
      abi.encodePacked(
            '\x19\x01',
            DOMAIN_SEPARATOR,
            keccak256(abi.encode(
                PERMIT_TYPEHASH, 
                owner, 
                spender, 
                value, 
                nonces[owner]++, 
                deadline
            ))
        )
    );
    

    sha256: External systems can hash payment details (sha256) and pass the hash (expectedHash) to the smart contract.

     // The contract verifies that the provided payment data matches the expected hash, ensuring data integrity across systems.
     contract PaymentProcessor {
        function verifyPayment(bytes32 expectedHash, string memory paymentData) public pure returns (bool) {
            // Recalculate hash of payment data
            bytes32 computedHash = sha256(abi.encodePacked(paymentData));
            // Compare with the expected hash (e.g., received from an external system)
            return computedHash == expectedHash;
        }
    }
    

34. How would you create a minimal proxy contract using the CREATE2 opcode in Solidity?

  • A minimal proxy (also known as an EIP-1167 clone factory) is a contract that delegates all calls to a master implementation contract using delegatecall. You can combine this with CREATE2 to deploy proxies at predictable addresses.
  • Steps:

    • Compute the address using keccak256.
    • Deploy the proxy contract using CREATE2 with a minimal bytecode proxy.
    • Example minimal proxy:
        bytes memory bytecode = abi.encodePacked(
            hex"363d3d373d3d3d363d73", masterContractAddress, hex"5af43d82803e903d91602b57fd5bf3"
        );
        address proxyAddress;
        assembly {
            proxyAddress := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        }
    

35. How does Solidity handle fixed-size and dynamic-size arrays differently in terms of gas usage and storage?

  • Fixed-size arrays have predefined storage slots allocated for each element at compile time, making them cheaper and faster for storage and access.
  • Dynamic-size arrays can grow or shrink, so additional storage and pointer management are required, resulting in higher gas costs for operations like adding or removing elements.
  • Trade-offs:
    • Use fixed-size arrays when array size is known in advance and performance is critical.
    • Use dynamic-size arrays when flexibility is needed.

36. What is extcodesize, and how can it be used to check if a contract has been deployed?

  • extcodesize is an EVM opcode that returns the size of the bytecode at a given address. It’s used to determine whether an address is a contract.
  • Example:
    function isContract(address account) internal view returns (bool) {
        uint256 size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }
Enter fullscreen mode Exit fullscreen mode
  • Use case:
    • Helps to prevent sending Ether to externally owned accounts (EOAs) when a contract address is expected.
    • Cons: Can’t differentiate between contracts in construction or destroyed contracts (selfdestructed contracts will return zero size).

37. What is the log opcode, and how are Solidity events translated into EVM logs?

  • log opcodes (log0, log1, up to log4) represent events in Solidity. They generate EVM logs that are stored on-chain but are not accessible within smart contracts themselves.
  • Use cases:
    - Used for emitting events, which are critical for off-chain dApps to monitor contract state changes.

    • Example of event emission:

          event Transfer(address indexed from, address indexed to, uint256 value);
          emit Transfer(msg.sender, recipient, amount);
      
  • Gas considerations: Storing data in logs is cheaper than in storage, but still requires gas.

38. What are low-level call, delegatecall, and staticcall, and when should you use each?

  • call: Executes code in another contract, allows Ether transfer, and changes context to the called contract.
  • delegatecall: Executes code in the context of the calling contract (proxy pattern).
  • staticcall: Similar to call, but ensures no state changes, useful for read-only external calls.
  • Use cases:
    • call for interacting with other contracts, transferring Ether, or fallback functions.
    • delegatecall for proxies and contract upgrades.
    • staticcall for ensuring no state modifications in read-only external calls.

39. How does the Solidity receive function differ from the fallback function, and when would you implement each?

  • receive: A specific function triggered when the contract receives Ether without data.
  • fallback: Triggered when a function is called that does not exist in the contract or when the contract receives Ether with data (if receive is not defined).
  • Use cases:

    • Implement receive for contracts that should accept Ether without any accompanying data.
    • Use fallback for advanced behavior like proxy contract routing or handling unexpected function calls.
    • Example:

      receive() external payable { }
      fallback() external payable { }
      

40. What is assembly in Solidity, and when would you use inline assembly (Yul)?

  • Assembly (Yul) allows for low-level manipulation of the EVM using opcodes directly. It's used for optimizations, gas savings, or accessing functionality not directly available in Solidity.
  • Use cases:
    • Gas optimization for complex arithmetic or loops.
    • Direct access to low-level EVM opcodes (e.g., mstore, mload).
  • Example:

    assembly {
        chainId := chainid
    }
    

    Ex2

    function add(uint x, uint y) public pure returns (uint) {
        assembly {
            let result := add(x, y)
            return(result, 32)
        }
    }
    
  • Trade-offs:

    • Can significantly reduce gas costs.
    • Harder to read, maintain, and secure compared to high-level Solidity code.

41. What are the gas optimizations you would apply when writing Solidity code, and what trade-offs do they introduce?

  • Gas optimizations include reducing storage writes, using memory instead of storage, minimizing contract size, and packing variables.
    • Pros:
      • Reduces transaction costs, making your contract more efficient.
      • Improves the performance of decentralized applications (dApps).
    • Cons:
      • Over-optimizing can make code harder to read and maintain.
      • Optimizations like variable packing might introduce bugs if improperly handled (e.g., overflow issues).

42. What are the challenges of using the selfdestruct function for contract upgrades or termination?

  • The selfdestruct function deletes a contract from the blockchain and transfers any remaining Ether to a specified address.
    • Pros:
      • Removes the contract, freeing up storage space on the blockchain.
      • Can be used to return funds in emergency situations.
    • Cons:
      • Leaves orphaned contract calls, which may cause other contracts interacting with it to fail.
      • There’s no way to undo a selfdestruct once executed.

43. What are storage collision attacks, and how do you prevent them when using delegatecall in Solidity?

  • Storage collisions occur when the storage layout between the proxy contract and the implementation contract differs, potentially causing state corruption.
    • Pros of preventing it:
      • Ensures the integrity of contract storage and logic.
    • Cons:
      • Requires careful design and auditing of storage layout across contract upgrades.
      • Incorrect handling can still lead to subtle, hard-to-detect bugs.

44. How would you implement a time-lock mechanism in a smart contract, and what are its potential pitfalls?

  • A time-lock restricts access to certain functions or assets for a predetermined time period, typically used for vesting or governance delays.
    • Pros:
      • Improves security by allowing a delay between action and execution, giving time for review.
      • Can prevent impulse or malicious actions.
    • Cons:
      • May lead to unintended delays or freezing of assets if poorly implemented.
      • Increases complexity, potentially making the system harder to interact with.

45. What is Solidity’s immutable keyword, and how does it compare with constant?

  • The immutable keyword defines a variable that is set during contract deployment and cannot be changed after that, but is not stored in storage.
    • Pros:
      • Reduces gas costs since immutable variables are stored in contract bytecode rather than storage.
      • Allows flexibility compared to constant, which must be known at compile-time.
    • Cons:
      • Once set, it cannot be changed, leading to potential limitations if the contract requires dynamic behavior later.

46. How do you secure a multi-signature contract, and what are the potential vulnerabilities?

  • Multi-signature contracts require multiple parties to sign off on a transaction before it is executed, adding a layer of security.
    • Pros:
      • Reduces the risk of a single point of failure or malicious control.
      • Offers decentralized governance and enhanced security for high-value assets.
    • Cons:
      • Introduces coordination challenges among signers, leading to delays.
      • Vulnerable to denial-of-service (DoS) attacks if some signers become inactive or maliciously refuse to sign.

47. Why prefer Use call over send, transfer?

The reason for using call instead of the other alternatives like transfer or send is mostly due to certain limitations and flexibility:

  • transfer: Transfers a fixed amount of gas (2300 gas) to the recipient, which is usually enough for basic Ether receipt but may fail if the receiving contract has more complex logic.
  • send: Similar to transfer but returns a boolean indicating success or failure.
  • call: Allows specifying the exact amount of gas and sending data. Since Solidity 0.6.x, call has become the recommended way to send Ether due to gas limitations imposed on transfer.

48. Explain the purpose of ERC-2612 and how it enhances the ERC-20 standard.

ERC-2612 introduces the permit function, enabling gasless approvals for ERC-20 tokens. Users can approve token transfers via cryptographic signatures off-chain, avoiding the need to perform an on-chain transaction. This is especially useful for DeFi applications as it reduces transaction costs and allows for meta-transactions where the approval and transfer are bundled.

49. How does the permit function work in ERC-2612, and what are its key parameters?

The permit function allows an owner to authorize a spender to spend tokens without requiring an on-chain transaction from the owner.

  • Key Parameters:
    • owner: The address authorizing the permit.
    • spender: The address allowed to spend the tokens.
    • value: The token amount allowed for spending.
    • deadline: A timestamp after which the permit is no longer valid.
    • (v, r, s): The components of the owner's ECDSA signature.
  • Example flow:
    • The owner signs a message off-chain using EIP-712.
    • The spender submits the permit function with the signature to the smart contract.
    • The contract verifies the signature and updates the allowance accordingly.

50. What is EIP-712, and why is it important for structured data hashing and signing in Solidity?

EIP-712 is a standard for encoding structured data into a message for signing, ensuring the signature cannot be reused across other contracts or domains. It improves security and usability by providing type-safe and domain-specific data signing. This is particularly critical for applications like permit, where signatures are used to authorize specific operations on a given chain and contract.

52. Describe the steps involved in generating an EIP-712 compliant signature in Solidity.

  1. Define Types: Create a type structure for the data to be signed.
  2. Hash the Data: Use abi.encode and keccak256 to hash the type and its fields.
  3. Construct the DOMAIN_SEPARATOR: Combine contract-specific information like name, version, and chainId to hash the domain.
  4. Combine the Domain and Data Hash: Use keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)) to create the final message hash.
  5. Sign the Hash: Use an off-chain wallet to generate the signature (via eth_signTypedData).
  6. Recover Signer: On-chain, verify the signature using ecrecover.

53. What is the DOMAIN_SEPARATOR in EIP-712, and how is it constructed?

The DOMAIN_SEPARATOR is a hashed structure that uniquely identifies a contract, preventing signature reuse across different domains or chains.
Construction:

DOMAIN_SEPARATOR = keccak256(
    abi.encode(
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
        keccak256(bytes(name)),
        keccak256(bytes(version)),
        chainId,
        address(this)
    )
);
Enter fullscreen mode Exit fullscreen mode

Fields include:

  • name: Contract name.
  • version: Contract version.
  • chainId: Blockchain ID.
  • verifyingContract: Contract address.

54. Why is the DOMAIN_SEPARATOR necessary in contracts using EIP-712?

It ensures signatures are valid only for a specific contract and chain, mitigating cross-contract replay attacks. Without a domain separator, a signature valid on one contract could be reused on another, compromising security.

55. How do nonces and deadlines protect against replay attacks in smart contracts?

  • Nonces: Each signature is tied to a unique incrementing number (nonce). Once used, the nonce is incremented, invalidating the signature for subsequent reuse.
  • Deadlines: The deadline specifies the validity period for a signature, ensuring it cannot be used indefinitely.

55. Write a Solidity snippet that verifies a permit using a nonce and deadline. Explain each step.

function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    require(deadline >= block.timestamp, "Expired deadline");
    bytes32 structHash = keccak256(
        abi.encode(
            PERMIT_TYPEHASH,
            owner,
            spender,
            value,
            nonces[owner]++,
            deadline
        )
    );
    bytes32 hash = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
    address signer = ecrecover(hash, v, r, s);
    require(signer == owner, "Invalid signature");
    _approve(owner, spender, value);
}
Enter fullscreen mode Exit fullscreen mode
  • Step 1: Check the deadline for expiration.
  • Step 2: Increment and encode the nonce.
  • Step 3: Hash the permit data and domain for signature verification.
  • Step 4: Verify the signer using ecrecover.

56. What is calldata in Solidity, and when should it be used over memory or storage?

  • calldata is a read-only data location for function parameters in external functions.
  • It is gas-efficient since it does not involve copying or modifying data.
  • Use it when passing immutable data to an external function, especially arrays or strings, where gas savings are critical.

57. What are the trade-offs of using calldata for function parameters in Solidity?

  • Pros: Gas-efficient, immutable, secure.
  • Cons: Cannot modify data, requiring additional memory allocation if modifications are necessary.

58. Explain the process of recovering a signer’s address from a hashed message and signature in Solidity.

  • Use keccak256 to hash the message.
  • Prefix the hash with "\x19Ethereum Signed Message:\n32" to match the eth_sign format.
  • Use ecrecover with (v, r, s) to recover the signer’s address.
    Example:

    function recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
        return ecrecover(hash, v, r, s);
    }
    

59. What are the potential security pitfalls of using ecrecover in Solidity? How can these be mitigated?

  • Pitfalls:
    • Malformed signatures may lead to unexpected results.
    • Lack of proper hashing allows replay attacks.
  • Mitigations:
    • Always hash messages with keccak256 and use EIP-712 standards for structured data.
    • Add domain-specific information to hashes to prevent reuse across chains/contracts.

These common and advanced questions delve deeper into Solidity's low-level mechanisms, optimization techniques, and EVM behavior, probing the candidate's understanding of performance, security, and the inner workings of smart contracts.

If you found this helpful, let me know by leaving a 👍 or a comment!, or if you think this post could help someone, feel free to share it! Thank you very much! 😃

💖 đŸ’Ș 🙅 đŸš©
truongpx396
Truong Phung

Posted on November 14, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related