Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,23 @@ jobs:
if: env.GIT_DIFF
run: |
go test -tags=test -coverprofile=profile-wasmbinding.txt -covermode=atomic ./wasmbinding/...
- name: rewards coverage (test build tag)
if: env.GIT_DIFF
run: |
go test -tags=test -coverprofile=profile-rewards.txt -covermode=atomic ./x/rewards/...
Comment on lines +54 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewards as not being covered by the topic above? Why the new entry here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reason as wasmbinding: rewards keeper tests are behind -tags=test. The main coverage job runs without that tag, so those packages don’t show up in Codecov unless we run a tagged profile (and upload it). Happy to fold into the primary step with -tags=test instead if you prefer one job.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why are we using the test tag on the testing files instead of scoping them as test files?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are scoped as test files, every one of them is _test.go. The tag isn't gating our test code, it's required by the cosmos/evm dependency. Any test that boots the full app (our apptesting/helpers wire the EVM module) calls EVMConfigurator.ResetTestConfig(). In cosmos/evm that function is a stub that panics with "this is only implemented with the 'test' build flag", The working implementation only exists in their test-tagged file. So without -tags=test the rewards keeper suite panics at setup rather than failing to compile.
That's why make test and make test-unit already pass -tags=test. The CI coverage step is the one place that doesn't, so rewards packages produce no coverage there. Same situation wasmbinding already worked around with its own tagged profile.

Alternative solution is add -tags=test to the primary coverage step and delete both extra entries (rewards + wasmbinding). I avoided that only because it changes coverage for the whole repo in one go.

- uses: actions/upload-artifact@v4
if: env.GIT_DIFF
with:
name: "${{ github.sha }}-coverage"
path: ./profile.txt
path: |
./profile.txt
./profile-wasmbinding.txt
./profile-rewards.txt
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
if: env.GIT_DIFF
with:
files: ./profile.txt,./profile-wasmbinding.txt
files: ./profile.txt,./profile-wasmbinding.txt,./profile-rewards.txt
token: ${{ secrets.CODECOV_TOKEN }}

repo-analysis:
Expand Down
7 changes: 3 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

### Added

- Emit `update_params`, `fund_pool`, `change_schedule`, and `reward_distributed` events from x/rewards
- Replace timed linear `ReleaseSchedule` emissions in `x/rewards` with continuous inflation-based utility rewards driven by bonded ratio: `inflation = clamp((1 - bonded/goal) × 0.13 × bonded, min, max)`, amount = `inflation × supply_base × Δt / year`, capped at remaining pool balance (emits until pool runs dry). Adds staking `BondedRatio` dependency and gov params `goal_bonded`, `inflation_min`, `inflation_max`, `supply_base` (default `0` disables emissions); `inflation_rate_change` is hardcoded at `0.13`. Enable via `MsgFundPool` + gov `MsgUpdateParams`
Comment thread
mattkii marked this conversation as resolved.
Outdated
- Emit `update_params`, `fund_pool`, and `reward_distributed` events from x/rewards (`reward_distributed` now includes `inflation_rate` and `bonded_ratio`)
Comment thread
mattkii marked this conversation as resolved.
Outdated
- Emit `update_params` and `set_denom_metadata` events from tokenfactory
- Emit `update_params`, `update_fee_tokens`, `module_disabled` and `token_disabled` events on fee abstraction
- Emit `update_params` event on oracle module
Expand All @@ -20,13 +21,11 @@
- Compute the oracle ballot `StandardDeviation` as a stake-weighted variance (weight each squared deviation by the vote's power and divide by total voting power) instead of an unweighted average divided by the vote count, aligning the reward-band width with the stake-weighted median and preventing a group of low-stake validators from inflating the deviation to widen the accepted vote window
- Close an oracle slashing bypass in the `EndBlocker` where validators were scored against the post-filtered `voteTargets` map: a denom that received votes but was pushed below the vote threshold (e.g. by a coordinated group abstaining) was dropped from the scoring denominator, letting the abstainers avoid miss penalties. Participation is now scored against the configured targets that received votes (passing targets plus below-threshold targets), crediting validators that voted on a below-threshold target while counting abstention on it as a miss; targets that received no votes at all are still excluded so a legitimately unpriceable denom cannot mass-slash the validator set
- Allow EIP-7702 delegated EOAs to send direct EVM transactions by exempting delegation-designator code from the externally-owned-account-only check in `VerifyIfAccountExists`, so accounts that delegate via `SetCodeTx` can still manage (and revoke) their own delegation without a sponsored transaction
- Remove the forced minimum 1-unit-per-block reward release in `CalculateReward` and skip (instead of deactivating) sub-unit blocks in the rewards `BeginBlocker`, so the proportional share accumulates and the pool follows the configured schedule independent of block time (previously a 10-year, 1M-unit schedule drained in ~12 days at the 1s target block time and ~28 days at the current ~2.4s rate, regardless of the configured duration)
- Reject `MsgEthereumTx` from being dispatched through the authz keeper (including when nested inside `authz.MsgExec`), closing an EVM ante bypass on message-router execution paths that skip the ante handler
- Fix feegrant denomination bypass in the cosmos fee ante handler by converting the fee before consuming the grant, so `UseGrantedFees` is checked against the same coins later deducted (prevents a grantee from forcing the granter to pay in a non-granted fee-abstraction denom)
- Refactor `PerformSetMetadata` in wasmbinding to delegate to `msgServer.SetDenomMetadata`, ensuring the `EnableSetMetadata` capability check is enforced
- Ensure that `UpdateTokenMetadata.Decimals` matches the ERC20 or bank records
- Fixed odd validation on tokenfactory change admin that blocked removing admin from the token
- Fix division-by-zero chain halt in `CalculateReward` caused by sub-second schedule durations; replace `Seconds()` truncation with `Nanoseconds()` precision and release full remaining reward when `EndTime <= LastReleaseTime` ([#267](https://github.com/KiiChain/kiichain/issues/267))
- Add denom string length validation (max 128 bytes) to oracle precompile and query server to prevent memory exhaustion via oversized inputs
- Add result limits to oracle list queries (ExchangeRates, Actives, VoteTargets capped at 1000; PriceSnapshotHistory capped at 500) to prevent unbounded iteration
- Fix NewClaim constructor assigning power to Weight field instead of the weight parameter (x/oracle/types/ballot.go)
Expand All @@ -44,11 +43,11 @@
- Indexed admins to reduce query space on tokenfactory denom queries
- Fix native token supply inflation from the stateful precompiles by wrapping the account address codec (`evmAddressCodec`) to reject non-20-byte accounts (e.g. a 32-byte bech32 withdraw, module, or CosmWasm contract address) at decode time, preventing such addresses from being truncated and minted a duplicate balance when mirrored into the EVM StateDB
- Close governance vote minimum-stake bypass in `GovVoteDecorator` by enforcing the stake check on `MsgVoteWeighted` (`govv1` and `govv1beta1`) and recursing into nested `authz.MsgExec` messages so wrapped votes can no longer skip the requirement
- Prevent a chain halt in the rewards `BeginBlocker` by routing `SendCoinsFromModuleToModule` failures through `haltSchedule` (graceful schedule deactivation) instead of returning a fatal error, matching the other reward release error paths
- Add a `ValidateModuleAccounting` check (rewards module bank balance must cover the `CommunityPool`) and run it at genesis to surface accounting/bank divergences early

### Removed

- Remove `ReleaseSchedule`, `MsgChangeSchedule`, and the release-schedule query/CLI from `x/rewards`; emissions are continuous while `supply_base > 0` and the pool has funds (`last_release_time` / `total_released` live on `RewardPool`)
- Removed price field input in updateTokenMetadata request

## v7.1.0-mainnet - 2026-03-13
Expand Down
1 change: 1 addition & 0 deletions app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ func NewAppKeeper(
appCodec,
runtime.NewKVStoreService(appKeepers.keys[rewardstypes.StoreKey]),
appKeepers.BankKeeper,
appKeepers.StakingKeeper,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
authtypes.FeeCollectorName,
)
Expand Down
297 changes: 297 additions & 0 deletions contrib/scripts/test_rewards_manual.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
#!/usr/bin/env bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be turned into 2e2 test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it is already. tests/e2e/e2e_rewards_test.go (TestRewards then fund pool then gov supply_base then assert pool drain / rewards). contrib/scripts/test_rewards_manual.sh is optional local smoke without the full e2e harness. Fine to drop the script if you’d rather keep only CI e2e.

# Manual smoke test for inflation-based x/rewards emissions.
#
# Flow:
# 1. Build + init a single-validator localnet (short gov voting period)
# 2. Fund the rewards pool
# 3. Pass MsgUpdateParams with supply_base > 0
# 4. Wait a few blocks and assert pool decreased / total_released increased
#
# Usage:
# ./contrib/scripts/test_rewards_manual.sh
# KEEP_HOME=1 ./contrib/scripts/test_rewards_manual.sh # leave node home for inspection
#
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"

BIN="${BIN:-$ROOT_DIR/build/kiichaind}"
HOME_DIR="${HOME_DIR:-$HOME/.kiichaind-rewards-manual}"
CHAIN_ID="${CHAIN_ID:-localchain_1010-1}"
KEY="${KEY:-val}"
DENOM="akii"
RPC_URL="http://127.0.0.1:26657"
NODE="tcp://127.0.0.1:26657"
# Standard gov module account (bech32 "kii" prefix)
GOV_AUTHORITY="kii10d07y265gmmuvt4z0w9aw880jnsr700jrff0qv"

FUND_AMOUNT="1000000000000000000000${DENOM}" # 1000 kii
SUPPLY_BASE="1000000000000000000000000" # 1e24
DEPOSIT_AMOUNT="10000000${DENOM}"
GAS_PRICES="3000000000${DENOM}"

log() { printf '\n==> %s\n' "$*"; }
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

broadcast_tx() {
local out code txhash
out="$("$@" --broadcast-mode sync -o json)"
code="$(echo "$out" | jq -r '.code // 0')"
txhash="$(echo "$out" | jq -r '.txhash // empty')"
if [[ "$code" != "0" ]]; then
echo "$out" | jq . >&2 || echo "$out" >&2
die "tx rejected at broadcast (code=$code)"
fi
[[ -n "$txhash" ]] || die "missing txhash from broadcast"
# Wait for inclusion
local i status
for i in $(seq 1 30); do
if status="$("$BIN" q tx "$txhash" --node "$NODE" -o json 2>/dev/null)"; then
code="$(echo "$status" | jq -r '.code // 0')"
if [[ "$code" != "0" ]]; then
echo "$status" | jq . >&2
die "tx $txhash failed on-chain (code=$code)"
fi
return 0
fi
sleep 1
done
die "timed out waiting for tx $txhash"
}

cleanup() {
if [[ -n "${NODE_PID:-}" ]] && kill -0 "$NODE_PID" 2>/dev/null; then
log "Stopping local node (pid $NODE_PID)"
kill "$NODE_PID" 2>/dev/null || true
wait "$NODE_PID" 2>/dev/null || true
fi
if [[ "${KEEP_HOME:-0}" != "1" ]]; then
rm -rf "$HOME_DIR"
else
log "Keeping home dir at $HOME_DIR (KEEP_HOME=1)"
fi
}
trap cleanup EXIT

need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}

need_cmd jq
need_cmd curl
Comment thread
coderabbitai[bot] marked this conversation as resolved.
need_cmd python3

log "Building kiichaind"
make build >/dev/null
[[ -x "$BIN" ]] || die "binary not found at $BIN"

# Free ports if a previous run left a node up
pkill -f "kiichaind.*${HOME_DIR}" >/dev/null 2>&1 || true
sleep 1
rm -rf "$HOME_DIR"

log "Initializing chain at $HOME_DIR"
"$BIN" init rewards-manual --chain-id "$CHAIN_ID" --home "$HOME_DIR" >/dev/null 2>&1
"$BIN" config set client chain-id "$CHAIN_ID" --home "$HOME_DIR"
"$BIN" config set client keyring-backend test --home "$HOME_DIR"
"$BIN" config set client node "$NODE" --home "$HOME_DIR"

"$BIN" keys add "$KEY" --home "$HOME_DIR" --keyring-backend test >/dev/null 2>&1
"$BIN" genesis add-genesis-account "$KEY" \
"10000000000000000000000000000000000000${DENOM}" \
--home "$HOME_DIR" --keyring-backend test >/dev/null

"$BIN" genesis gentx "$KEY" "1000000000000000000000${DENOM}" \
--home "$HOME_DIR" --chain-id "$CHAIN_ID" --keyring-backend test >/dev/null 2>&1
"$BIN" genesis collect-gentxs --home "$HOME_DIR" >/dev/null 2>&1

GENESIS="$HOME_DIR/config/genesis.json"
APP_TOML="$HOME_DIR/config/app.toml"

# Short gov window + matching EVM denom / metadata (same as start-localnet-ci)
tmp="$(mktemp)"
jq '
.app_state.gov.params.voting_period = "12s"
| .app_state.gov.params.expedited_voting_period = "8s"
| .app_state.gov.params.min_deposit = [{"denom":"akii","amount":"10000000"}]
| .app_state.gov.params.expedited_min_deposit = [{"denom":"akii","amount":"10000000"}]
| .app_state.gov.params.quorum = "0.000000000000000001"
| .app_state.gov.params.threshold = "0.000000000000000001"
| .app_state.evm.params.evm_denom = "akii"
| .app_state.bank.denom_metadata = [{
"description":"The native staking token of the kiichain network",
"denom_units":[{"denom":"akii","exponent":0},{"denom":"kii","exponent":18}],
"base":"akii","display":"kii","name":"kii","symbol":"KII"
}]
' "$GENESIS" > "$tmp" && mv "$tmp" "$GENESIS"

if [[ "$(uname)" == "Darwin" ]]; then
sed -i.bak 's/minimum-gas-prices = ""/minimum-gas-prices = "0akii"/' "$APP_TOML"
else
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0akii"/' "$APP_TOML"
fi
# Keep feemarket from blocking with a high floor relative to our gas prices
tmp="$(mktemp)"
jq '
.app_state.feemarket.params.no_base_fee = true
| .app_state.feemarket.params.min_gas_price = "0.000000000000000000"
| .app_state.feemarket.params.base_fee = "0.000000000000000000"
' "$GENESIS" > "$tmp" && mv "$tmp" "$GENESIS"

log "Starting node"
"$BIN" start --home "$HOME_DIR" >"$HOME_DIR/node.log" 2>&1 &
NODE_PID=$!

# Wait for RPC
for i in $(seq 1 60); do
if curl -sf "$RPC_URL/status" >/dev/null 2>&1; then
break
fi
sleep 1
if [[ $i -eq 60 ]]; then
tail -n 80 "$HOME_DIR/node.log" || true
die "node did not become ready"
fi
done
# Wait until height > 1
for i in $(seq 1 30); do
height="$(curl -sf "$RPC_URL/status" | jq -r '.result.sync_info.latest_block_height')"
if [[ "$height" =~ ^[0-9]+$ ]] && (( height > 1 )); then
log "Node ready at height $height"
break
fi
sleep 1
if [[ $i -eq 30 ]]; then
die "node never reached height > 1"
fi
done

TX_FLAGS=(
--home "$HOME_DIR"
--keyring-backend test
--chain-id "$CHAIN_ID"
--node "$NODE"
--gas 5000000
--gas-prices "$GAS_PRICES"
--yes
)

log "Query initial rewards params / pool"
"$BIN" q rewards params --node "$NODE" -o json | jq .
INITIAL_POOL="$("$BIN" q rewards reward-pool --node "$NODE" -o json)"
echo "$INITIAL_POOL" | jq .
INITIAL_SUPPLY_BASE="$(
"$BIN" q rewards params --node "$NODE" -o json | jq -r '.params.supply_base // .supply_base // "0"'
)"
[[ "$INITIAL_SUPPLY_BASE" == "0" || "$INITIAL_SUPPLY_BASE" == "0.000000000000000000" ]] \
|| die "expected default supply_base=0, got $INITIAL_SUPPLY_BASE"

log "Fund rewards pool with $FUND_AMOUNT"
broadcast_tx "$BIN" tx rewards fund-pool "$FUND_AMOUNT" --from "$KEY" "${TX_FLAGS[@]}"

FUNDED_POOL="$("$BIN" q rewards reward-pool --node "$NODE" -o json)"
echo "$FUNDED_POOL" | jq .
FUNDED_AMOUNT="$(echo "$FUNDED_POOL" | jq -r '
(.reward_pool.community_pool // .community_pool // [])
| map(select(.denom=="akii")) | .[0].amount // "0"
')"
[[ "$FUNDED_AMOUNT" != "0" && "$FUNDED_AMOUNT" != "null" ]] || die "pool still empty after fund-pool"
log "Pool funded amount=$FUNDED_AMOUNT"

PROPOSAL_FILE="$HOME_DIR/proposal_update_rewards_params.json"
cat >"$PROPOSAL_FILE" <<EOF
{
"messages": [
{
"@type": "/kiichain.rewards.v1beta1.MsgUpdateParams",
"authority": "$GOV_AUTHORITY",
"params": {
"token_denom": "akii",
"goal_bonded": "0.670000000000000000",
"inflation_min": "0.000000000000000000",
"inflation_max": "0.200000000000000000",
"supply_base": "$SUPPLY_BASE"
}
}
],
"metadata": "ipfs://CID",
"deposit": "$DEPOSIT_AMOUNT",
"title": "Enable Rewards Emissions",
"summary": "set supply_base to enable inflation-based emissions"
}
EOF

log "Submit + vote MsgUpdateParams (supply_base=$SUPPLY_BASE)"
broadcast_tx "$BIN" tx gov submit-proposal "$PROPOSAL_FILE" --from "$KEY" "${TX_FLAGS[@]}"
PROPOSAL_ID="$("$BIN" q gov proposals --node "$NODE" -o json | jq -r '.proposals | max_by(.id | tonumber) | .id')"
[[ -n "$PROPOSAL_ID" && "$PROPOSAL_ID" != "null" ]] || die "could not find proposal id"
log "Proposal id=$PROPOSAL_ID"
broadcast_tx "$BIN" tx gov vote "$PROPOSAL_ID" yes --from "$KEY" "${TX_FLAGS[@]}"

log "Waiting for proposal to pass"
PASSED=0
for i in $(seq 1 40); do
STATUS="$("$BIN" q gov proposal "$PROPOSAL_ID" --node "$NODE" -o json | jq -r '.proposal.status // .status')"
log "Proposal status=$STATUS (wait $i)"
if [[ "$STATUS" == "PROPOSAL_STATUS_PASSED" || "$STATUS" == "3" ]]; then
PASSED=1
break
fi
if [[ "$STATUS" == "PROPOSAL_STATUS_REJECTED" || "$STATUS" == "PROPOSAL_STATUS_FAILED" || "$STATUS" == "4" || "$STATUS" == "5" ]]; then
"$BIN" q gov proposal "$PROPOSAL_ID" --node "$NODE" -o json | jq .
die "proposal did not pass"
fi
sleep 2
done
[[ "$PASSED" -eq 1 ]] || die "timed out waiting for proposal"

UPDATED_SUPPLY_BASE="$(
"$BIN" q rewards params --node "$NODE" -o json | jq -r '.params.supply_base // .supply_base'
)"
log "Updated supply_base=$UPDATED_SUPPLY_BASE"
[[ "$UPDATED_SUPPLY_BASE" == "$SUPPLY_BASE" ]] || die "supply_base not updated"

# First begin-blocker after enable only stamps last_release_time; wait for releases
log "Waiting for emissions across several blocks"
sleep 12

FINAL_POOL="$("$BIN" q rewards reward-pool --node "$NODE" -o json)"
echo "$FINAL_POOL" | jq .
FINAL_AMOUNT="$(echo "$FINAL_POOL" | jq -r '
(.reward_pool.community_pool // .community_pool // [])
| map(select(.denom=="akii")) | .[0].amount // "0"
')"
TOTAL_RELEASED="$(echo "$FINAL_POOL" | jq -r '
(.reward_pool.total_released.amount // .total_released.amount // "0")
')"
LAST_RELEASE="$(echo "$FINAL_POOL" | jq -r '
(.reward_pool.last_release_time // .last_release_time // "")
')"

log "Funded amount = $FUNDED_AMOUNT"
log "Final amount = $FINAL_AMOUNT"
log "Total released= $TOTAL_RELEASED"
log "Last release = $LAST_RELEASE"

# Compare as integers (strip decimal portion from DecCoin strings)
python3 - "$FUNDED_AMOUNT" "$FINAL_AMOUNT" "$TOTAL_RELEASED" <<'PY'
import sys

def to_int(s: str) -> int:
s = s.strip()
if "." in s:
s = s.split(".", 1)[0]
return int(s)

funded = to_int(sys.argv[1])
final = to_int(sys.argv[2])
released = to_int(sys.argv[3])
if not (final < funded):
raise SystemExit(f"pool did not decrease: funded={funded} final={final}")
if not (released > 0):
raise SystemExit(f"total_released not positive: {released}")
print(f"OK: pool decreased by {funded - final}, total_released={released}")
PY

log "Manual rewards smoke test PASSED"
8 changes: 4 additions & 4 deletions proto/kiichain/rewards/v1beta1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ syntax = "proto3";
package kiichain.rewards.v1beta1;

import "gogoproto/gogo.proto";
import "cosmos/base/v1beta1/coin.proto";
import "kiichain/rewards/v1beta1/params.proto";
import "kiichain/rewards/v1beta1/types.proto";

Expand All @@ -13,9 +12,10 @@ message GenesisState {
// params defines the parameters of the module.
Params params = 1 [ (gogoproto.nullable) = false ];

// release_schedule has information of how the reward is being released
ReleaseSchedule release_schedule = 2 [ (gogoproto.nullable) = false ];
// Reserved: previously release_schedule
reserved 2;
reserved "release_schedule";

// reward_pool has information on the community pool
RewardPool reward_pool = 3 [ (gogoproto.nullable) = false ];
}
}
Loading
Loading