Skip to content

Commit da2a316

Browse files
authored
Merge pull request #631 from CosmWasm/update-ics20-contract
Update ics20 contract
2 parents 01595c1 + 3839633 commit da2a316

File tree

6 files changed

+314
-76
lines changed

6 files changed

+314
-76
lines changed

contracts/cw20-ics20/src/contract.rs

Lines changed: 156 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
#[cfg(not(feature = "library"))]
22
use cosmwasm_std::entry_point;
33
use cosmwasm_std::{
4-
from_binary, to_binary, Addr, Binary, Deps, DepsMut, Env, IbcMsg, IbcQuery, MessageInfo, Order,
5-
PortIdResponse, Response, StdResult,
4+
ensure_eq, from_binary, to_binary, Addr, Binary, Deps, DepsMut, Env, IbcMsg, IbcQuery,
5+
MessageInfo, Order, PortIdResponse, Response, StdResult,
66
};
77

88
use cw2::{get_contract_version, set_contract_version};
99
use cw20::{Cw20Coin, Cw20ReceiveMsg};
10+
use cw_storage_plus::Bound;
1011

1112
use crate::amount::Amount;
1213
use crate::error::ContractError;
1314
use crate::ibc::Ics20Packet;
1415
use crate::msg::{
15-
ChannelResponse, ExecuteMsg, InitMsg, ListChannelsResponse, MigrateMsg, PortResponse, QueryMsg,
16-
TransferMsg,
16+
AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, ExecuteMsg, InitMsg,
17+
ListAllowedResponse, ListChannelsResponse, MigrateMsg, PortResponse, QueryMsg, TransferMsg,
1718
};
18-
use crate::state::{Config, CHANNEL_INFO, CHANNEL_STATE, CONFIG};
19+
use crate::state::{AllowInfo, Config, ALLOW_LIST, CHANNEL_INFO, CHANNEL_STATE, CONFIG};
1920
use cw_utils::{nonpayable, one_coin};
2021

2122
// version info for migration info
@@ -32,8 +33,18 @@ pub fn instantiate(
3233
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
3334
let cfg = Config {
3435
default_timeout: msg.default_timeout,
36+
gov_contract: deps.api.addr_validate(&msg.gov_contract)?,
3537
};
3638
CONFIG.save(deps.storage, &cfg)?;
39+
40+
// add all allows
41+
for allowed in msg.allowlist {
42+
let contract = deps.api.addr_validate(&allowed.contract)?;
43+
let info = AllowInfo {
44+
gas_limit: allowed.gas_limit,
45+
};
46+
ALLOW_LIST.save(deps.storage, &contract, &info)?;
47+
}
3748
Ok(Response::default())
3849
}
3950

@@ -50,6 +61,7 @@ pub fn execute(
5061
let coin = one_coin(&info)?;
5162
execute_transfer(deps, env, msg, Amount::Native(coin), info.sender)
5263
}
64+
ExecuteMsg::Allow(allow) => execute_allow(deps, env, info, allow),
5365
}
5466
}
5567

@@ -81,11 +93,18 @@ pub fn execute_transfer(
8193
return Err(ContractError::NoFunds {});
8294
}
8395
// ensure the requested channel is registered
84-
// FIXME: add a .has method to map to make this faster
85-
if CHANNEL_INFO.may_load(deps.storage, &msg.channel)?.is_none() {
96+
if !CHANNEL_INFO.has(deps.storage, &msg.channel) {
8697
return Err(ContractError::NoSuchChannel { id: msg.channel });
8798
}
8899

100+
// if cw20 token, ensure it is whitelisted
101+
if let Amount::Cw20(coin) = &amount {
102+
let addr = deps.api.addr_validate(&coin.address)?;
103+
ALLOW_LIST
104+
.may_load(deps.storage, &addr)?
105+
.ok_or(ContractError::NotOnAllowList)?;
106+
};
107+
89108
// delta from user is in seconds
90109
let timeout_delta = match msg.timeout {
91110
Some(t) => t,
@@ -103,7 +122,7 @@ pub fn execute_transfer(
103122
);
104123
packet.validate()?;
105124

106-
// prepare message
125+
// prepare ibc message
107126
let msg = IbcMsg::SendPacket {
108127
channel_id: msg.channel,
109128
data: to_binary(&packet)?,
@@ -124,6 +143,48 @@ pub fn execute_transfer(
124143
Ok(res)
125144
}
126145

146+
/// The gov contract can allow new contracts, or increase the gas limit on existing contracts.
147+
/// It cannot block or reduce the limit to avoid forcible sticking tokens in the channel.
148+
pub fn execute_allow(
149+
deps: DepsMut,
150+
_env: Env,
151+
info: MessageInfo,
152+
allow: AllowMsg,
153+
) -> Result<Response, ContractError> {
154+
let cfg = CONFIG.load(deps.storage)?;
155+
ensure_eq!(info.sender, cfg.gov_contract, ContractError::Unauthorized);
156+
157+
let contract = deps.api.addr_validate(&allow.contract)?;
158+
let set = AllowInfo {
159+
gas_limit: allow.gas_limit,
160+
};
161+
ALLOW_LIST.update(deps.storage, &contract, |old| {
162+
if let Some(old) = old {
163+
// we must ensure it increases the limit
164+
match (old.gas_limit, set.gas_limit) {
165+
(None, Some(_)) => return Err(ContractError::CannotLowerGas),
166+
(Some(old), Some(new)) if new < old => return Err(ContractError::CannotLowerGas),
167+
_ => {}
168+
};
169+
}
170+
Ok(AllowInfo {
171+
gas_limit: allow.gas_limit,
172+
})
173+
})?;
174+
175+
let gas = if let Some(gas) = allow.gas_limit {
176+
gas.to_string()
177+
} else {
178+
"None".to_string()
179+
};
180+
181+
let res = Response::new()
182+
.add_attribute("action", "allow")
183+
.add_attribute("contract", allow.contract)
184+
.add_attribute("gas_limit", gas);
185+
Ok(res)
186+
}
187+
127188
#[cfg_attr(not(feature = "library"), entry_point)]
128189
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
129190
let version = get_contract_version(deps.storage)?;
@@ -141,6 +202,11 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
141202
QueryMsg::Port {} => to_binary(&query_port(deps)?),
142203
QueryMsg::ListChannels {} => to_binary(&query_list(deps)?),
143204
QueryMsg::Channel { id } => to_binary(&query_channel(deps, id)?),
205+
QueryMsg::Config {} => to_binary(&query_config(deps)?),
206+
QueryMsg::Allowed { contract } => to_binary(&query_allowed(deps, contract)?),
207+
QueryMsg::ListAllowed { start_after, limit } => {
208+
to_binary(&list_allowed(deps, start_after, limit)?)
209+
}
144210
}
145211
}
146212

@@ -183,6 +249,59 @@ pub fn query_channel(deps: Deps, id: String) -> StdResult<ChannelResponse> {
183249
})
184250
}
185251

252+
fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
253+
let cfg = CONFIG.load(deps.storage)?;
254+
let res = ConfigResponse {
255+
default_timeout: cfg.default_timeout,
256+
gov_contract: cfg.gov_contract.into(),
257+
};
258+
Ok(res)
259+
}
260+
261+
fn query_allowed(deps: Deps, contract: String) -> StdResult<AllowedResponse> {
262+
let addr = deps.api.addr_validate(&contract)?;
263+
let info = ALLOW_LIST.may_load(deps.storage, &addr)?;
264+
let res = match info {
265+
None => AllowedResponse {
266+
is_allowed: false,
267+
gas_limit: None,
268+
},
269+
Some(a) => AllowedResponse {
270+
is_allowed: true,
271+
gas_limit: a.gas_limit,
272+
},
273+
};
274+
Ok(res)
275+
}
276+
277+
// settings for pagination
278+
const MAX_LIMIT: u32 = 30;
279+
const DEFAULT_LIMIT: u32 = 10;
280+
281+
fn list_allowed(
282+
deps: Deps,
283+
start_after: Option<String>,
284+
limit: Option<u32>,
285+
) -> StdResult<ListAllowedResponse> {
286+
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
287+
let start = match start_after {
288+
Some(x) => Some(Bound::exclusive(deps.api.addr_validate(&x)?.into_string())),
289+
None => None,
290+
};
291+
292+
let allow = ALLOW_LIST
293+
.range(deps.storage, start, None, Order::Ascending)
294+
.take(limit)
295+
.map(|item| {
296+
item.map(|(addr, allow)| AllowedInfo {
297+
contract: addr.into(),
298+
gas_limit: allow.gas_limit,
299+
})
300+
})
301+
.collect::<StdResult<_>>()?;
302+
Ok(ListAllowedResponse { allow })
303+
}
304+
186305
#[cfg(test)]
187306
mod test {
188307
use super::*;
@@ -195,7 +314,7 @@ mod test {
195314

196315
#[test]
197316
fn setup_and_query() {
198-
let deps = setup(&["channel-3", "channel-7"]);
317+
let deps = setup(&["channel-3", "channel-7"], &[]);
199318

200319
let raw_list = query(deps.as_ref(), mock_env(), QueryMsg::ListChannels {}).unwrap();
201320
let list_res: ListChannelsResponse = from_binary(&raw_list).unwrap();
@@ -230,7 +349,7 @@ mod test {
230349
#[test]
231350
fn proper_checks_on_execute_native() {
232351
let send_channel = "channel-5";
233-
let mut deps = setup(&[send_channel, "channel-10"]);
352+
let mut deps = setup(&[send_channel, "channel-10"], &[]);
234353

235354
let mut transfer = TransferMsg {
236355
channel: send_channel.to_string(),
@@ -242,6 +361,7 @@ mod test {
242361
let msg = ExecuteMsg::Transfer(transfer.clone());
243362
let info = mock_info("foobar", &coins(1234567, "ucosm"));
244363
let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();
364+
assert_eq!(res.messages[0].gas_limit, None);
245365
assert_eq!(1, res.messages.len());
246366
if let CosmosMsg::Ibc(IbcMsg::SendPacket {
247367
channel_id,
@@ -289,9 +409,9 @@ mod test {
289409
#[test]
290410
fn proper_checks_on_execute_cw20() {
291411
let send_channel = "channel-15";
292-
let mut deps = setup(&["channel-3", send_channel]);
293-
294412
let cw20_addr = "my-token";
413+
let mut deps = setup(&["channel-3", send_channel], &[(cw20_addr, 123456)]);
414+
295415
let transfer = TransferMsg {
296416
channel: send_channel.to_string(),
297417
remote_address: "foreign-address".to_string(),
@@ -307,6 +427,7 @@ mod test {
307427
let info = mock_info(cw20_addr, &[]);
308428
let res = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap();
309429
assert_eq!(1, res.messages.len());
430+
assert_eq!(res.messages[0].gas_limit, None);
310431
if let CosmosMsg::Ibc(IbcMsg::SendPacket {
311432
channel_id,
312433
data,
@@ -330,4 +451,27 @@ mod test {
330451
let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
331452
assert_eq!(err, ContractError::Payment(PaymentError::NonPayable {}));
332453
}
454+
455+
#[test]
456+
fn execute_cw20_fails_if_not_whitelisted() {
457+
let send_channel = "channel-15";
458+
let mut deps = setup(&["channel-3", send_channel], &[]);
459+
460+
let cw20_addr = "my-token";
461+
let transfer = TransferMsg {
462+
channel: send_channel.to_string(),
463+
remote_address: "foreign-address".to_string(),
464+
timeout: Some(7777),
465+
};
466+
let msg = ExecuteMsg::Receive(Cw20ReceiveMsg {
467+
sender: "my-account".into(),
468+
amount: Uint128::new(888777666),
469+
msg: to_binary(&transfer).unwrap(),
470+
});
471+
472+
// works with proper funds
473+
let info = mock_info(cw20_addr, &[]);
474+
let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
475+
assert_eq!(err, ContractError::NotOnAllowList);
476+
}
333477
}

contracts/cw20-ics20/src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ pub enum ContractError {
4949

5050
#[error("Got a submessage reply with unknown id: {id}")]
5151
UnknownReplyId { id: u64 },
52+
53+
#[error("You cannot lower the gas limit for a contract on the allow list")]
54+
CannotLowerGas,
55+
56+
#[error("Only the governance contract can do this")]
57+
Unauthorized,
58+
59+
#[error("You can only send cw20 tokens that have been explicitly allowed by governance")]
60+
NotOnAllowList,
5261
}
5362

5463
impl From<FromUtf8Error> for ContractError {

0 commit comments

Comments
 (0)