🌈Flexible Environment

One other important security layer needed in DeFi is to make dApps as independent as possible from an off-chain company. Projects in Ethereum need to manage bugs and updates. Usually, coders will go about this by issuing a proxy contract and using their private key to update the code if needed.

This approach does not guarantee total security for dApp users, because their funds are usually in the hands of the dApp issuers. dApp issuers are not a bank! Remember there are no bailouts or legitimate refunds in crypto. Trusting a dApp with code driven by a single company, even if they're regulated in the real world, is always a bad idea. Some dApps are developing DAOs to mitigate this risk, but at the same time a DAO can reduce problems by voting. Because the code of the DAO is in the hands of issuers, this is better, but still not a concrete solution.

On the contrary, there are new approaches to tackle this problem - like Decentralized Flexible Organizations (DFOs) - that remove the need for issuers to have special keys by using a protocol that is 100% modular and only upgradable or managed by voting.

contract MVDProxy {

    //... Some code

    function newProposal(string memory codeName, bool emergency, address sourceLocation, uint256 sourceLocationId, address location, bool submitable, string memory methodSignature, string memory returnAbiParametersArray, bool isInternal, bool needsSender, string memory replaces) public override returns(address proposalAddress) {
        emergencyBehavior(emergency);

        IMVDFunctionalityModelsManager(_delegates[3]).checkWellKnownFunctionalities(codeName, submitable, methodSignature, returnAbiParametersArray, isInternal, needsSender, replaces);

        IMVDFunctionalitiesManager functionalitiesManager = IMVDFunctionalitiesManager(_delegates[4]);

        IMVDFunctionalityProposal proposal = IMVDFunctionalityProposal(proposalAddress = IMVDFunctionalityProposalManager(_delegates[1]).newProposal(codeName, location, methodSignature, returnAbiParametersArray, replaces));
        proposal.setCollateralData(emergency, sourceLocation, sourceLocationId, submitable, isInternal, needsSender, msg.sender, functionalitiesManager.hasFunctionality("getVotesHardCap") ? toUint256(read("getVotesHardCap", "")) : 0);

        if(functionalitiesManager.hasFunctionality("onNewProposal")) {
            submit("onNewProposal", abi.encode(proposalAddress));
        }

        if(!IMVDFunctionalitiesManager(_delegates[4]).hasFunctionality("startProposal") || !IMVDFunctionalitiesManager(_delegates[4]).hasFunctionality("disableProposal")) {
            proposal.start();
        }

        emit Proposal(proposalAddress);
    }

    //... Other code
}

This is where it all begins: the creation of a Proposal is carried out by calling the Proxy of the DFO, which in turn consults its delegates, who physically take care of controlling, creating and keeping track of all the data of the Surveys that the Token Holders vote on.

contract MVDProxy {

    //... Some code

    function setProposal() public override {

        IMVDFunctionalityProposalManager(_delegates[1]).checkProposal(msg.sender);

        emit ProposalCheck(msg.sender);

        IMVDFunctionalitiesManager functionalitiesManager = IMVDFunctionalitiesManager(_delegates[4]);

        (address addressToCall,,string memory methodSignature,,) = functionalitiesManager.getFunctionalityData("checkSurveyResult");

        (bool surveyResult, bytes memory response) = addressToCall.staticcall(abi.encodeWithSignature(methodSignature, msg.sender));

        surveyResult = toUint256(response) > 0;

        IMVDFunctionalityProposal proposal = IMVDFunctionalityProposal(msg.sender);

        if(!surveyResult) {
            proposal.set();
            emit ProposalSet(msg.sender, surveyResult);            
            return;
        }

        if(collateralCallResult) {
            try functionalitiesManager.setupFunctionality(msg.sender) returns(bool managerResult) {
                collateralCallResult = managerResult;
            } catch {
                collateralCallResult = false;
            }
        }
        
        if(collateralCallResult) {
            proposal.set();
            emit ProposalSet(msg.sender, collateralCallResult);
        }
    }

    //... Other code
}

When a user finalizes a Vote Proposal, it automatically calls the Proxy of the DFO, which in turn asks its delegates to verify the result of the calling Proposal (via the "checkSurveyResult" Microservice) and then call, if successful, the finalization of the Microservice (via its Functionalities Manager).

contract MVDFunctionalitiesManager {

  function setupFunctionality(address proposalAddress) public override returns(bool result) {

        require(_proxy == msg.sender, "Only Proxy can call This!");

        IMVDFunctionalityProposal proposal = IMVDFunctionalityProposal(proposalAddress);

        string memory codeName = proposal.getCodeName();
        bool hasCodeName = !compareStrings(codeName, "");
        string memory replaces = proposal.getReplaces();
        bool hasReplaces = !compareStrings(replaces, "");

        if(!hasCodeName && !hasReplaces) {
            (result,) = IMVDProxy(_proxy).callFromManager(_callingContext = proposal.getLocation(), abi.encodeWithSignature("callOneTime(address)", proposalAddress));
            _callingContext = address(0);
            return result;
        }
        //continue with other cases...
    }
}

On the other hand, the Microservice setup can only be performed exclusively if the FunctionalitiesManager is called by the Proxy, as you can see from the 'require' sentence in the code.

If the conditions are met, the FunctionalitiesManager calls the Proxy again to execute the Microservice code. In summary, we have Token Holders voting for a Proposal, which triggers a constrained automated process that leads to its finalization, thanks to the orchestrated collaboration between all the Contracts that make up a DFO, following two fundamental principles of Software Engineering: 'Divide Et Impera' and 'Single Responsibility'.

This new experimental approach can create a new DeFi standard, by making dApps as resilient as ever and independent from the issuer which is to say the issuer can only rule the code if he holds tokens. Crucially, in the case of issuer failure, the dApp can still continue to function and be updated by token holders. more info at https://dfohub.com

Flexible environments are cool. Nevertheless, they need some limitations to be totally fair for DeFi dApps:

Voting Limitations:

Token holders in a DFO can vote to run code, so they can vote to manage every asset and function. Some things, however, shouldn't be changed by voting. When building a DeFi application on top of a DFO, you need to create an external Functionality to store user funds.

For example, in the code of the Liquidity Staking Contract, all the values ​​that identify the times and parameters for generating the rewards of the various tiers are entered in the construction phase, and it is no longer possible to change them. Rewards are automatically sent to the External Smart Contract, so DFOs can only change the rules for newcomers and not for already staked positions.

Every time a new staking position will be opened, the staking contract will receive from the DFO the voting tokens necessary for the reward, by calling the "stakingTransfer" Microservice, as seen in the method called calculateRewardAndAddStakingPosition. These tokens will be transferable only by the owner of the position, according to the desired times.

contract Stake {

    //Other code...

    constructor(uint256 startBlock, address doubleProxy, address[] memory tokens, uint256[] memory timeWindows, uint256[] memory rewardMultipliers, uint256[] memory rewardDividers, uint256[] memory rewardSplitTranches) public {

        _startBlock = startBlock;

        _doubleProxy = doubleProxy;

        for(uint256 i = 0; i < tokens.length; i++) {
            TOKENS.push(tokens[i]);
        }

        assert(timeWindows.length == rewardMultipliers.length && rewardMultipliers.length == rewardDividers.length && rewardDividers.length == rewardSplitTranches.length);
        for(uint256 i = 0; i < timeWindows.length; i++) {
            TIME_WINDOWS.push(timeWindows[i]);
        }

        for(uint256 i = 0; i < rewardMultipliers.length; i++) {
            REWARD_MULTIPLIERS.push(rewardMultipliers[i]);
        }

        for(uint256 i = 0; i < rewardDividers.length; i++) {
            REWARD_DIVIDERS.push(rewardDividers[i]);
        }

        for(uint256 i = 0; i < rewardSplitTranches.length; i++) {
            REWARD_SPLIT_TRANCHES.push(rewardSplitTranches[i]);
        }
    }

    //Other code...

    function calculateRewardAndAddStakingPosition(uint256 tier, uint256 poolPosition, uint256 firstAmount, uint256 secondAmount, uint256 poolAmount, IMVDProxy proxy) private {
        uint256 partialRewardSingleBlockTime = TIME_WINDOWS[tier] / REWARD_SPLIT_TRANCHES[tier];
        uint256[] memory partialRewardBlockTimes = new uint256[](REWARD_SPLIT_TRANCHES[tier]);
        if(partialRewardBlockTimes.length > 0) {
            partialRewardBlockTimes[0] = block.number + partialRewardSingleBlockTime;
            for(uint256 i = 1; i < partialRewardBlockTimes.length; i++) {
                partialRewardBlockTimes[i] = partialRewardBlockTimes[i - 1] + partialRewardSingleBlockTime;
            }
        }
        uint256 reward = firstAmount * REWARD_MULTIPLIERS[tier] / REWARD_DIVIDERS[tier];
        StakeInfo memory stakeInfo = StakeInfo(msg.sender, poolPosition, firstAmount, secondAmount, poolAmount, reward, block.number + TIME_WINDOWS[tier], partialRewardBlockTimes, reward / REWARD_SPLIT_TRANCHES[tier]);
        _add(tier, stakeInfo);
        proxy.submit("stakingTransfer", abi.encode(address(0), 0, reward, address(this)));
        emit Staked(msg.sender, tier, poolPosition, firstAmount, secondAmount, poolAmount, reward, stakeInfo.endBlock, partialRewardBlockTimes, stakeInfo.splittedReward);
    }

    //Other code...
}
function stakingTransfer(address sender, uint256, uint256 value, address receiver) public {
    IMVDProxy proxy = IMVDProxy(msg.sender);
    require(IStateHolder(proxy.getStateHolderAddress()).getBool(_toStateHolderKey("staking.transfer.authorized", _toString(sender))), "Unauthorized action!");
    proxy.transfer(receiver, value, proxy.getToken());
}

In turn, the stakingTransfer Microservice will check that it is being called only and exclusively by an active staking contract. Therefore, if the community of token holders chooses to have new staking rules, they will vote to deactivate the old staking contract and activate the new one with new rules. By doing so, all the staking positions of the old staking contract will always be available and redeemable, but it will not be possible to open new positions. At the same time, the new staking contract with the new rules will work normally.

Last updated