Presto

Built on top of the Covenants Aggregator, and integrable with all Covenant functions, Presto optimizes and automates tasks that require multiple swap, transfer and add liquidity operations, making everything simpler, faster and cheaper for users.

This section is dedicated to the general-purpose Presto contract, which can be customized for specific use cases, as demonstrated by our Presto verticalizations for the Covenant WUSD, Farming and Index Token protocols. See their respective sections to learn more.

How Presto Works

The building block of each Presto contract is the PrestoOperation struct, which contains all of the requisite data for an operation.

struct PrestoOperation {

    address inputTokenAddress;
    uint256 inputTokenAmount;

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

    address[] receivers;
    uint256[] receiversPercentages;
}

Its inputs are as follows:

  • inputTokenAddress -> the address of the input token (i.e. the initial token) for swap and transfer operations; for addLiquidity operations, the address of the liquidity pool (LP) token.

  • inputTokenAmount -> the amount of the input (or LP) token to be used in the operation.

  • ammPlugin -> the address of the Automated Market Maker (AMM) for swap and addLiquidity operations; for transfer operations this field is not populated.

  • liquidityPoolAddresses -> array containing the LP addresses to be used for a swap operation (not populated for transfer or addLiquidity operations).

  • swapPath -> array containing the sequential path of tokens (ending with the output token) used in a swap operation. So for example if you have an operation that swaps BUIDL to ETH and then to USDC, the swapPath array will contain ETH in the first position and USDC in the second one.This field is left blank for non-swap operations.

  • enterInEth -> a boolean value that represents if the input token is ETH (true) or not (false); i.e, if the input token is ETH, enterInETH is true; if not, enterInETH is false.

  • exitInEth -> a boolean value that represents if the output token is ETH (true) or not (false); i.e, if the output token is ETH, exitInETH is true; if not, exitInETH is false.

  • receivers -> array containing the receiver addresses of the operation. Receiver addresses can be defined for swap, transfer and addLiquidity operations.

  • receiversPercentages -> array containing the relative percentage for each addressreceiver. The length of this array must be equal to receivers array length -1; since the last receiver's percentage is equal to the outstanding percentage, it is calculated as that automatically.

Execute Presto Operations

To perform Presto operations as defined by their structs, the execute function is used, input with an array of one or more PrestoOperation structs; multiple can be executed at once.

function execute(PrestoOperation[] memory operations) public override payable {

The execute function first has to transfer the inputTokenAmount from the msg.sender (i.e. the Presto user) to the Presto contract, which it does by calling the _transferToMe method.

_transferToMe(operations);

But before _transferToMe can transfer the inputTokenAmount, it must first internally call the _collectTokens method in order to calculate the amount to be transferred.

For addLiquidity operations, the _collectTokens method internally calls _byLiquidityPoolAmount, (an API of the AMM Aggregator) and uses it to calculate the amount of LP tokens.

if(operation.ammPlugin != address(0) && operation.liquidityPoolAddresses.length == 0) {
    IAMM amm = IAMM(operation.ammPlugin);
    (address ethereumAddress,,) = (amm.data());
    (uint256[] memory amounts, address[] memory tokensAddresses) = amm.byLiquidityPoolAmount(operation.inputTokenAddress, operation.inputTokenAmount);
    bool hasEth = false;
    for(uint256 z = 0; z < tokensAddresses.length; z++) {
        if(tokensAddresses[z] == ethereumAddress) {
            hasEth = true;
        }

Then it internally calls the _collectTokenData method, passing as input the addresses and amounts of paired tokens of the LP token.

_collectTokenData(operation.enterInETH && tokensAddresses[z] == ethereumAddress ? address(0) : tokensAddresses[z], amounts[z]);

For transfer and swap operations, the _collectTokens method also internally calls the _collectTokenData method, but passes as input the address and amount of the input token.

_collectTokenData(operation.ammPlugin != address(0) && operation.enterInETH ? address(0) : operation.inputTokenAddress, operation.inputTokenAmount);

The _collectTokenData function uses this input to create an indexed position for the inputTokenAddress; or, if one has already been created for another operation in the contract, retrieves and adds to it the amount of input token for this one, represented by _tokenAmounts[position]. This is so that if you want to perform multiple operations (by passing multiple PrestoOperation structs in the contract) with the same input token, the contract will only have to perform the token transfer once via _safeTransferFrom.

uint256 position = _tokenIndex[inputTokenAddress];
if(_tokensToTransfer.length == 0 || _tokensToTransfer[position] != inputTokenAddress) {
    _tokenIndex[inputTokenAddress] = (position = _tokensToTransfer.length);
    _tokensToTransfer.push(inputTokenAddress);
    _tokenAmounts.push(0);
 }
 _tokenAmounts[position] = _tokenAmounts[position] + inputTokenAmount;

With all parameters calculated, the_transferToMe method can, at last, carry out the execute function's original task, by internally calling the _safeTransferFrom method, which transfers the right amount of tokens from the msg.sender to the contract.

_safeTransferFrom(_tokensToTransfer[i], msg.sender, address(this), _tokenAmounts[i]);

And now the execute function can perform each operation.

1) Transfer

For a transfer operation, execute calls the _transferTo method. This calculates: 1) the amount of the input token to be sent to the Covenants DFO wallet, as a mandatory fee 2) the amounts of it to be sent to the receivers as specified in the receivers and receiversPercentages arrays It then transfers these amounts accordingly.

function _transferTo(address erc20TokenAddress, uint256 totalAmount, address[] memory receivers, uint256[] memory receiversPercentages) private {
    uint256 availableAmount = totalAmount;

    (uint256 dfoFeePercentage, address dfoWallet) = feePercentageInfo();
    uint256 currentPartialAmount = dfoFeePercentage == 0 || dfoWallet == address(0) ? 0 : _calculateRewardPercentage(availableAmount, dfoFeePercentage);
    _safeTransfer(erc20TokenAddress, dfoWallet, currentPartialAmount);
    availableAmount -= currentPartialAmount;

    uint256 stillAvailableAmount = availableAmount;

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

    _safeTransfer(erc20TokenAddress, receivers[receivers.length - 1], availableAmount);
    }

2) Add Liquidity

For addLiquidity operations, execute calls the _addLiquidity method. This uses the AMM Aggregator to build the liquidityPoolData to be called by the addLiquidity function (one of the AMM Aggregator on-chain API).

 LiquidityPoolData memory liquidityPoolData = LiquidityPoolData(
     operation.inputTokenAddress,
     operation.inputTokenAmount,
     address(0),
     true,
     operation.enterInETH,
     address(this)
);

The addLiquidity function then sends the obtained output LP tokens to the receivers as specified in the receivers andreceiversPercentages arrays.

(uint256 amountOut,,) = IAMM(operation.ammPlugin).addLiquidity(liquidityPoolData);
_transferTo(operation.inputTokenAddress, amountOut, operation.receivers, operation.receiversPercentages);

3) Swap

For swap operations, execute calls the _swap method. This first defines the output token of the operation.

address outputToken = operation.swapPath[operation.swapPath.length - 1];

and then uses the AMM Aggregator to build the swapData to be called by the swap function.

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

If necessary (i.e. if the input token isn't ETH), if then also internally calls the_safeApprove method on the input token.

if(swapData.inputToken != address(0) && !swapData.enterInETH) {
    _safeApprove(swapData.inputToken, operation.ammPlugin, swapData.amount);
}

and then calculates the amount of output token to be obtained from the swap operation, which it then performs.

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

and then finally sends that amount to the receivers as specified in the receivers and receiversPercentages arrays.

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

Flush and Clear After performing all of a contract's operations, the execute function finally calls the _flushAndClear method.

function _flushAndClear() private {

This takes care of two things. 1) It checks if any residual tokens remain in the contract, and if so sends them back to the msg.sender.

Example During the execution of an operation, market conditions change in such a way that not all tokens are sent to receivers. Let's say that in a swap 3 ETH is swapped for 5.1 BUIDL, and that the total balance is now 5.1 BUIDL—even though an LPAmount was set that corresponds to 5 BUIDL in the PrestoOperation. This discrepancy means that 0.1 BUIDL remains in the contract. The _flushAndClear method allows themsg.sender to retrieve these residual tokens so that none remain in the contract.

for(uint256 i = 0; i < _tokensToTransfer.length; i++) {
    _safeTransfer(_tokensToTransfer[i], msg.sender, _balanceOf(_tokensToTransfer[i]));
    delete _tokenIndex[_tokensToTransfer[i]];
}

2) It deletes the storage, _tokenAmounts and _tokenToTransfer parameters (which were used to efficiently manage transfer operations between the user and the contract) from the contract.

 delete _tokensToTransfer;
 delete _tokenAmounts;

Last updated