I have a smart contract that basically exposes two entry point. A stake and an unstake function.
In the local test, when a user deposits a token; it executes successfully and writes to the contract state but when I call the unstake function it executes without an Invoke error but state changes are not persisted after the contract call.
On the testnet, when I call the stake function, it successfully executes but no state changes are persisted. when I query the smart contract to view the stake it returns none and when I try to use the unstake function it returns a stake is not found error.
This is the stake function:
#[receive(
contract = "gona_stake",
name = "stake",
error = "StakingError",
parameter = "OnReceivingCis2DataParams<ContractTokenId,ContractTokenAmount,PublicKeyEd25519>",
enable_logger,
mutable
)]
fn stake(ctx: &ReceiveContext, host: &mut Host<State>, logger: &mut Logger) -> ReceiveResult<()> {
let parameter: OnReceivingCis2DataParams<
ContractTokenId,
ContractTokenAmount,
PublicKeyEd25519,
> = ctx.parameter_cursor().get()?;
let amount = parameter.amount;
let token_id = parameter.token_id;
let state = host.state_mut();
let gona_token = state.token_address;
let staker = parameter.data;
// Ensures that only contracts can call this hook function.
let sender_contract_address = match ctx.sender() {
Address::Contract(sender_contract_address) => sender_contract_address,
Address::Account(_) => bail!(StakingError::OnlyContractCanStake.into()),
};
// Cannot stake less than 0.001 of our token
ensure!(
amount.0.ge(&1000),
StakingError::CannotStakeLessThanAllowAmount.into()
);
ensure_eq!(
sender_contract_address,
gona_token,
StakingError::SenderContractAddressIsNotAllowedToStake.into()
);
let mut entry = StakeEntry {
amount,
time_of_stake: ctx.metadata().slot_time(),
token_id,
};
if let Some(stake_entry) = state.stake_entries.remove_and_get(&staker) {
let days_of_stake = ctx
.metadata()
.slot_time()
.duration_since(stake_entry.time_of_stake)
.ok_or(StakingError::DaysOfStakeCouldNotBeCalculated)?
.days();
let previous_amount = stake_entry.amount;
let rewards = calculate_percent(previous_amount.0, state.weight, state.decimals);
if days_of_stake > 0 {
let rewards = rewards * days_of_stake;
let new_amount = previous_amount + TokenAmountU64(rewards);
entry.amount = new_amount;
} else {
entry.amount += previous_amount;
}
stake_entry.delete();
state.stake_entries.entry(staker).or_insert(entry);
} else {
state.stake_entries.entry(staker).or_insert(entry);
}
logger.log(&StakingEvent::Staked {
staker,
amount,
time: ctx.metadata().slot_time(),
})?;
Ok(())
}
and this is the unstake function:
#[receive(
contract = "gona_stake",
name = "unstake",
error = "StakingError",
parameter = "UnstakeParam",
enable_logger,
mutable
)]
fn unstake(ctx: &ReceiveContext, host: &mut Host<State>, logger: &mut Logger) -> ReceiveResult<()> {
let param: UnstakeParam = ctx.parameter_cursor().get()?;
let state = host.state_mut();
let weight = state.weight;
let decimals = state.decimals;
let reward_volume = state.reward_volume;
let stake_entry = state
.stake_entries
.get(¶m.staker)
.ok_or(StakingError::StakingNotFound)?;
let previous_amount = stake_entry.amount;
ensure!(
previous_amount.0.ge(¶m.amount.0),
StakingError::InsufficientFunds.into()
);
let days_of_stake = ctx
.metadata()
.slot_time()
.duration_since(stake_entry.time_of_stake)
.ok_or(StakingError::DaysOfStakeCouldNotBeCalculated)?
.days();
let mut amount = param.amount;
let token_address = state.token_address;
let smart_wallet = state.smart_wallet;
// if days == 0 and you calculate reward. it will change balance to 0
if days_of_stake > 0 {
let rewards = calculate_percent(amount.0, weight, decimals);
let cumulative_rewards = rewards * days_of_stake;
ensure!(reward_volume >= rewards, StakingError::Overflow.into());
state.reward_volume -= cumulative_rewards;
amount += TokenAmountU64(cumulative_rewards);
//bail!(StakingError::TransferError.into());
}
let owned_entry = OwnedEntrypointName::new_unchecked("depositCis2Tokens".into());
// Create a Transfer instance
let transfer_payload = Transfer {
token_id: TOKEN_ID,
amount,
to: Receiver::Contract(smart_wallet, owned_entry),
from: Address::Contract(ctx.self_address()),
data: AdditionalData::from(to_bytes(¶m.staker)),
};
let entry_point = EntrypointName::new_unchecked("transfer".into());
let mut transfers = Vec::new();
transfers.push(transfer_payload);
let payload = TransferParams::from(transfers);
// calculate transfer after withdrawal; if amount is less than 0.001 flush the account
let balance = previous_amount.0 - param.amount.0;
if balance < 1000 {
state.stake_entries.remove(¶m.staker);
} else {
state
.stake_entries
.entry(param.staker.clone())
.and_modify(|stake| {
stake.amount = TokenAmountU64(balance);
});
}
host.invoke_contract(&token_address, &payload, entry_point, Amount::zero())?;
logger.log(&StakingEvent::Unstaking {
amount: param.amount,
staker: param.staker,
time: ctx.metadata().slot_time(),
})?;
Ok(())
}
This is my state:
#[concordium(state_parameter = "S")]
pub struct State<S = StateApi> {
pub stake_entries: StateMap<PublicKeyEd25519, StakeEntry, S>,
pub decimals: u8,
pub token_address: ContractAddress,
pub weight: u32,
pub paused: bool,
pub admin: Address,
pub smart_wallet: ContractAddress,
pub reward_volume: u64,
}
This is the stake entry param:
#[derive(Serialize, SchemaType, PartialEq, Eq, Clone, Debug)]
pub struct StakeEntry {
pub amount: TokenAmountU64,
pub time_of_stake: Timestamp,
pub token_id: TokenIdUnit,
}