Skip to content

Instantly share code, notes, and snippets.

@larry0x
Last active December 13, 2023 16:38
Show Gist options
  • Save larry0x/572273e86009b85fe2da15a3176a138c to your computer and use it in GitHub Desktop.
Save larry0x/572273e86009b85fe2da15a3176a138c to your computer and use it in GitHub Desktop.

The data field in cosmwasm_std::Response is underused. We should use it more! Here's why.

Often we want to program contracts that do this:

  1. call another contract
  2. after the call is completed, handle the response and do some extra actions

For example,

  1. swap some token A to token B at Astroport
  2. deposit the output B tokens into Mars

The problem here is that in step (2) we need to figure out what is the output B token amount from step (1), so that we can properly compose an execute message to make the deposit.

Unfortunately, a DEX swap output amount is typically not knowable beforehand (due to other users also making trades in the same block), so we have to find out the output amount in situ, by parsing the response that Astroport contract has returned.

How do we do this? Most developers I know choose to parse the events. For example, Astroport has a return_amount attribute in the event.

Here I argue this is the wrong way of doing this. Simple reason: events are not merklized, meaning validators don't reach consensus over them. Therefore they are not guaranteed to be deterministic. In principle, contracts should only have access to data that validators reach consensus over.

In fact, in cw-sdk I'm simply going to remove the events field from SubMsgResponse.

The right way to do it - Astroport should put necessary data into the data field, and our contract deserializes the data when handling the response:

// Astroport defines the response type
struct SwapResponse {
    pub offer_asset:       Uint128,
    pub ask_asset:         Uint128,
    pub return_amount:     Uint128,
    pub commission_amount: Uint128,
}
// in the Response, attach the data
fn swap(
    deps: DespMut,
    env: Env,
    info: MessageInfo,
    // ...
) -> StdResult<Response> {
    // ...

    let swap_res = SwapResponse {
        offer_asset,
        ask_asset,
        return_amount,
        commission_amount,
    };

    let swap_res_bin = to_binary(&response)?;

    Ok(Response::new()
        .add_data(swap_res_bin)
        // add messages and event attributes...
    )
}
// caller contract handles the data
fn after_swap(deps: DepsMut, submsg_res: SubMsgResponse) -> StdResult<Response> {
    let Some(swap_res_bin) = &submsg_res.data else {
        return Err(StdError::generic_err("response data is empty"));
    }

    let swap_res: SwapResponse = from_binary(swap_res_bin)?;

    let return_amount = swap_res.return_amount;

    // make deposit...
}

Here's a suggestion. We already have a QueryResponses macro; we should have one for executes as well!

#[cw_serde]
#[derive(ExecuteResponses)]
enum ExecuteMsg {
    #[returns(SwapResponse)]
    Swap {
        slippage_tolerance: Option<Decimal>,
    },

    #[returns(ProvideLiquidityResponse)]
    ProvideLiquidity {
        slippage_tolerance: Option<Decimal>,
    },
}

This enforces contracts to implement the responses and makes it easier for calling contracts to consume the responses.

@ccamel
Copy link

ccamel commented Jun 12, 2023

Really interresting. Thanks for sharing this. 😎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment