๐Ÿ’ŽSimple Is Better

Keep smart contracts simple; keep smart contracts secure.

Modular Programming:

Every part of a complex function should be split into standalone modules that can work together.

Code must "talk." A complex algorithm must be composed as a sequence of simple functions, one that can be read as a complete sentence by a stranger without the aid of documentation. If possible, these functions should work as standalone modules as well as together in unity.

function setDoubleProxy(address newDoubleProxy) public _byCommunity
{
    _doubleProxy = newDoubleProxy;
}

function setAllowedPairs(address[] memory newAllowedPairs) public _byCommunity
{
    _allowedPairs = newAllowedPairs;
}

The following setter methods contain a very simple logic, and both have the _byCommunity modifier:

modifier _byCommunity() {
    require(
        IMVDFunctionalitiesManager(
            IMVDProxy(IDoubleProxy(_doubleProxy).proxy())
                .getMVDFunctionalitiesManagerAddress()
        )
        .isAuthorizedFunctionality(msg.sender),
        "Unauthorized Action!"
    );
    _;
}

Its body calls the Functionalities Manager, passing on chain first from the DoubleProxy and then from the Proxy, ensuring that the caller of these setters is an authorized Functionality of the DFO to which this contract refers. For it to be an authorized Functionality, it must first of all have been Proposed through a Survey, which was then voted on by the community of token holders, who together accepted it to perform the action.

Simple Math:

Solve mathematical problems in the simplest way possible. If your solution is complex, it may include unnecessary points of failure, particularly in calculations of decimals vs Wei or actions based on prices. The Responsible DeFi approach is to always ask the questions, "does this dApp really need this level of complexity?" and "can this problem be solved with simpler calculations?"

Solidity is a great language, but still a nascent one, and it's not always easy to figure out what exactly is under the hood of an algorithm. To be truly sure of what you're doing, keep the logic as basic as possible. especially in the mathematical calculations, which are the most delicate part.

function _calculateRewardToMint(uint256 burntWei) private returns(uint256 rewardWei) {
    rewardWei = _standardRewardWei;
    rewardWei = rewardWei * (burntWei / (10**decimals()));
    rewardWei =  (rewardWei * _rebalanceRewardMultiplier[0]) / _rebalanceRewardMultiplier[1];
}

This code shows how it is possible to generate a reward starting from a base value, expressed in wei (18 decimal places), with the burnt tokens, also expressed in wei, by multiplying them by a certain exchange factor. To do this, the amount of tokens burned is converted into 'ether' (i.e. without decimals) in order to make the calculation consistent. But what happens when the amount of tokens burned is less than 1? There are no floating point numbers in Solidity, so converting to 'ether' will truncate the numbers after the comma, resulting in 0.

The best way to solve this problem is to make sure that the amount of tokens burned is always greater than or equal to 1 (i.e. 10 ^ 18), to avoid inconsistencies in the mathematics.

function rebalance() public override {
    uint256 burnable = calculateBurnable();
    uint256 available = balanceOf(msg.sender);
    burnable = available >= burnable ? burnable : burnable - available;
    require(burnable >= 10**decimals(), "Insufficient amount to burn");
    _burn(msg.sender, burnable);
    IMVDProxy(IDoubleProxy(_doubleProxy).proxy()).submit(
        "mintNewVotingTokens",
         abi.encode(
            address(0),
            0,
            _calculateRewardToMint(burnable),
            msg.sender
        )
    );
}

Don't Overuse the State:

The State on Ethereum is a very cool tool, but at the same time, it can be your worst nightmare. While developing on Ethereum, you must always keep in mind that you can't track everything; you have to allow anonymous people to use an application without needing to track them.

The Ethereum State can help you achieve more, but remember to think 999999 times before using it for unnecessary things, and always remember to generate the right limitation, not just because it's expensive for the users, but also because you can receive an OUT of GAS. Solidity allows you to save different types of data, both standard and customized.

The space available for the storage of a contract is vast, but keeping a lot of information can mean costly transactions! It is recommended to save exclusively those data that are useful to the contract itself or to the others from which it is called.

Everything else, such as information to populate the UI of the dApp of which the contract is a part, should be reconstructed externally, starting from both the few contract data points available, and the Events that the contract issues whenever it is called in a transaction.

function _emitEvent(address proxy, address sender) private {
    IMVDProxy(msg.sender).emitEvent("DFODeployed(address_indexed,address_indexed,address,address)", abi.encodePacked(proxy), abi.encodePacked(sender), abi.encode(proxy, sender));
}

Make Smart Contracts Readable:

If you follow all the steps outlined above, your smart contracts will be very simple to read, even for non-coders!

Documentation and comments are a cool approach to help people understand them, but humans make them, so trusting them is not the ideal way to understand the contract. Rendering the code legible to a layperson even without comments and documentation is a far better approach.

Last updated