How Contracts and Operations Work

Each Covenants inflation contract is composed of one or more executable operations. Each operation can only circulate one token; to manage the inflation of two different tokens, two different operations are required.

An operation can involve any combination of the following:

  • The minting of the input token

  • The transferral of the input token from one address to one or more others

  • The swapping of the input token for one or more other tokens in one AMM

If an operation involves a swap, the input token is the token being swapped , and the output token is the token the input token is being swapped for. So, for example, if the operation will swap BUIDL for UniFi, BUIDL is the input token and UniFi is the output token. If the operation involves a transfer, we have only one token, so the input-output dichotomy does not apply.

The contract is represented by the following struct in the contract:

struct FixedInflationEntry {
    string name;
    uint256 blockInterval;
    uint256 lastBlock;
    uint256 callerRewardPercentage;
}
  • name -> represents the name of the contract

  • blockInterval -> the interval of time (expressed in blocks) that must pass between each execution of an operation

  • lastBlock -> the block of the most recently executed operation. Can be also used to program a delayed operation start

  • callerRewardPercentage -> the reward the executor will receive. Allows the creation of an incentive for executing the operation with a public call, not only the call by the internal hosting of the contract. The executor can freely choose to be rewarded with either input or output tokens; for example, if the operation swaps BUIDL (input token) for ETH (output token), he can choose to be rewarded in either.

Every Fixed Inflation Operation has the following parameters:

struct FixedInflationOperation {

    address inputTokenAddress;
    uint256 inputTokenAmount;
    bool inputTokenAmountIsPercentage;
    bool inputTokenAmountIsByMint;

    address ammPlugin;
    address[] liquidityPoolAddresses;
    address[] swapPath;
    bool enterInETH;
    bool exitInETH;

    address[] receivers;
    uint256[] receiversPercentages;
}
  • inputTokenAddress -> address of the operations' input token

  • inputTokenAmount -> amount of the input token

  • inputTokenAmountIsPercentage -> boolean value that expresses whether the amount of the input token (previous field) is expressed as a percentage value (true) or not (false).

If the InputTokenAmountIsPercentage value is true, the percentage is expressed in reference to the total supply of the input token. If the value is false , only the amount is expressed.

  • ammPlugin -> address of the AMM chosen to be used for possible token swap operations, this field is equal to 0x0000000000000000000000000000000000000000 if the operation is a transfer.

  • liquidityPoolAddresses -> array containing the liquidity pool token addresses of a swap operation (so this field is only populated if a swap operation is involved). The first element of the array must necessarily contain the input token

  • swapPath -> This array contains the path that the swap operation must follow (so this field is only populated if a swap operation is involved). Please refers to AMM Aggregator section for more details

So for example if you have an operation that swaps BUIDL to ETH and then to USDC, you'll have the liquidityPoolAddresses containing BUIDL/ETH LP address in the fist position and ETH/USDC LP address in the second place. The swapPath array will contain ETH in the first position and USDC in the second one.

  • enterInEth -> expresses whether the input token is ETH (true) or not (false)

  • exitInEth -> expresses whether the output token is ETH (true) or not (false)

  • receivers -> array that contains the addresses of the various receivers in case of a transfer operation

  • receiversPercentages -> array that contains the percentages of the various receivers in case of transfer operation. The length of this array must be equal to receivers array length -1, the last percentage is in fact calculated automatically.

Transfer Operations

Transfer operations are managed through the _transferTo function, called internally at the time of execution:

function _transferTo(address erc20TokenAddress, uint256 totalAmount, address rewardReceiver, uint256 callerRewardPercentage, address[] memory receivers, uint256[] memory receiversPercentages) private {
  • erc20TokenAddress -> address of the token on which to perform the transfer operation

  • totalAmount -> token amount to transfer

  • rewardReceiver -> address that will receive the reward for calling the execution of the operation

  • callerRewardPercentage -> percentage of the transfer transaction the executor will receive as a reward

  • receivers -> array that contains the addresses of the receiver(s) of the transfer operation

  • receiversPercentages -> array that contains the percentages of the receiver(s) of the transfer operation

The function calculates the reward amount for the executor using the _calculateRewardPercentage function), sends it to the caller address and subtracts it from the total amount passed as totalAmount :

uint256 currentPartialAmount = rewardReceiver == address(0) ? 0 : _calculateRewardPercentage(availableAmount, callerRewardPercentage);
_transferTo(erc20TokenAddress, rewardReceiver, currentPartialAmount);
availableAmount -= currentPartialAmount;

Then, the amount of tokens to be sent as a fee to the Covenants DFO is calculated, sent to Covenants treasury and subtracted from the available token amount. So, at this moment, the token amount to be transferred is equal to the initial total amount - executor reward amount - DFO fee amount.

Below is the calculation of the amount of tokens to be transferred to each individual receiver via the _calculateRewardPercentage function and sent to their respective addresses:

for(uint256 i = 0; i < receivers.length - 1; i++) {
    _transferTo(erc20TokenAddress, receivers[i], currentPartialAmount = _calculateRewardPercentage(stillAvailableAmount, receiversPercentages[i]));
    availableAmount -= currentPartialAmount;
}
_transferTo(erc20TokenAddress, receivers[receivers.length - 1], availableAmount);

Swap Operations

Swap operations are managed through the _swap function, called internally at the time of execution:

function _swap(FixedInflationOperation memory operation, uint256 amountIn, address rewardReceiver, uint256 callerRewardPercentage, bool earnByInput) private {

If the executor's reward is requested in input token, the amount is immediately calculated through the _calculateRewardPercentage function.

Then, the SwapData to be used by the AMM aggregator for swap operations is created as follow:

SwapData memory swapData = SwapData(
    operation.enterInETH,
    operation.exitInETH,
    operation.liquidityPoolAddresses,
    operation.swapPath,
    operation.enterInETH ? ethereumAddress : operation.inputTokenAddress,
    amountIn - inputReward,
    address(this)
);

As you can see, the reward amount is expressed as the initial passed amount to be swapped minus the reward in case the reward is requested as input token.

Then, the swap operation is executed through the AMM Aggregator swapLiquidity function, using the newly created swapData:

if(swapData.enterInETH) {
   amountOut = IAMM(operation.ammPlugin).swapLiquidity{value : amountIn}(swapData);
} else {
   amountOut = IAMM(operation.ammPlugin).swapLiquidity(swapData);
}

At the end, the reward percentage is transferred to the caller, whether it was requested in input token

if(earnByInput) {
   _transferTo(operation.enterInETH ? address(0) : operation.inputTokenAddress, rewardReceiver, inputReward);
}

or output token

_transferTo(operation.exitInETH ? address(0) : outputToken, amountOut, earnByInput ? address(0) : rewardReceiver, earnByInput ? 0 : callerRewardPercentage, operation.receivers, operation.receiversPercentages);

Last updated