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:
- call another contract
- after the call is completed, handle the response and do some extra actions
For example,
- swap some token A to token B at Astroport
- 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.
this is actually good, thanks for writing this ๐