Code is law
Smart Contract Address: addr1wygd2q56lc098fn0yrx9n6ngrjfjylxefxqztas6fffz7dsl4y9kn
spending GameReward
/*
Create a datum with the benefitiary PubKeyHash that will be included
into each UTXO locked at this script address.
*/
struct Datum {
benefitiary: PubKeyHash
}
/*
The vesting contract can be either canceled by the benefitiary
or claimed by the beneficiary
*/
enum Redeemer {
Cancel
Claim {
recepiant: PubKeyHash
}
}
const TOKEN_POLICY_ID: MintingPolicyHash = MintingPolicyHash::new(#5b9006e5051296968c46a3e9206f2f02c8157ff041871290960d6adf)
const HALVING_PERIOD: Int = 7776000 // 3 months in seconds
const MAX_HALVINGS: Int = 2
const BASE_REWARD: Int = 10000 // Initial reward in tokens
func calculate_reward(current_time: Time) -> Int {
// Start contract at
TimeBeginContract: Time = Time::new(1736073600000);
// Calculate time elapsed in seconds
time_elapsed: Duration = current_time - TimeBeginContract;
time_elapsed_in_seconds: Int = time_elapsed / Duration::new(1_000); // Convert to seconds
// Calculate the number of halving periods (each period is 1 month)
halving_steps: Int = time_elapsed_in_seconds / HALVING_PERIOD;
print(current_time.show());
// Ensure the number of halvings doesn't exceed the maximum allowed
halving_steps = if (halving_steps > MAX_HALVINGS) { MAX_HALVINGS } else { halving_steps };
print(halving_steps.show());
// Calculate reward using simple multiplication and division
if (halving_steps == 0) {
BASE_REWARD
} else if (halving_steps == 1) {
BASE_REWARD / 2
} else if (halving_steps == 2) {
BASE_REWARD / 4
} else {
BASE_REWARD / 4
}
}
const CLAIM_WINDOW: Int = 20
const CYCLE_DURATION: Int = 600
func IsClaimWindow(current_time: Time) -> Bool {
// Start contract
TimeBeginContract: Time = Time::new(1736073600000);
// Calculate the offset from the deployment start
elapsed_time: Duration = current_time - TimeBeginContract;
time_elapsed_in_seconds: Int = elapsed_time / Duration::new(1_000); // Convert to seconds
// Determine the position within the current cycle
position_in_cycle: Int = time_elapsed_in_seconds % CYCLE_DURATION;
in_claim_window: Bool = position_in_cycle <= CLAIM_WINDOW;
print(position_in_cycle.show());
in_claim_window
}
// Define the main validator function
func main(datum: Datum, redeemer: Redeemer, ctx: ScriptContext) -> Bool {
tx: Tx = ctx.tx;
// AssetClass for the treasury tokens
asset_class: AssetClass = AssetClass::new(
TOKEN_POLICY_ID,
"BERT".encode_utf8()
);
validator_hash: ValidatorHash = ctx.get_current_validator_hash();
// Get all outputs locked at the script address
script_outputs: []TxOutput = tx.outputs_locked_by(validator_hash);
total_tokens_begin: Int = tx.inputs.fold(
(sum: Int, input: TxInput) -> Int {
sum + input.output.value.get_safe(asset_class)
},
0
);
// Sum up all tokens in these outputs
total_tokens_in_script: Int = script_outputs.fold(
(sum: Int, output: TxOutput) -> Int {
sum + output.value.get_safe(asset_class)
},
0 // Initial sum
);
// Calculate total ADA in script inputs
total_ada_begin: Int = tx.inputs.fold((sum: Int, input: TxInput) -> Int {
if (input.output.address.credential.switch {
vh_credential: Validator => vh_credential.hash == validator_hash,
_ => false
}) {
sum + input.output.value.get_lovelace()
} else {
sum
}
}, 0);
// Calculate total ADA in script outputs
total_ada_in_script: Int = tx.outputs.fold((sum: Int, output: TxOutput) -> Int {
if (output.address.credential.switch {
vh_credential: Validator => vh_credential.hash == validator_hash,
_ => false
}) {
sum + output.value.get_lovelace()
} else {
sum
}
}, 0);
current_time: Time = tx.time_range.start;
// Calculate dynamic reward based on the remaining supply
dynamic_reward: Int = calculate_reward(current_time);
isClaimWindow: Bool = IsClaimWindow(current_time);
//expected_value: Value = Value::new(asset_class, EXPECTED_AMOUNT);
// Depending on the redeemer provided in the transaction, process accordingly.
redeemer.switch {
Cancel => {
// Tx must be signed by pkh in datum
tx.is_signed_by(datum.benefitiary)
},
red: Claim => {
// Determine how many tokens to give
tokens_to_give: Int = if (dynamic_reward > total_tokens_begin) { total_tokens_begin } else { dynamic_reward };
assert(total_tokens_begin - tokens_to_give == total_tokens_in_script, "Not enough tokens sent back to script");
// Check that the user receives exactly tokens_to_give
assert(tx.value_sent_to(red.recepiant).get_safe(asset_class) == tokens_to_give, "Incorrect token payout");
assert(isClaimWindow == true, "Not in claim window");
// Now handle ADA and script outputs logic
if (total_tokens_in_script == 0) {
// Final claim scenario: no tokens remain
// All ADA can be returned to the claimant, so no script outputs allowed
assert(script_outputs.length == 0, "No script outputs should remain at final claim");
// In this scenario, we don't need to enforce total_ada_in_script >= total_ada_begin,
// since all ADA should now be leaving the script and going to the claimant.
true
} else {
// Not final claim: tokens remain in the script
// Require at least 2 script outputs for concurrency (adjust as needed)
assert(script_outputs.length == 2, "I need 2 outputs");
// Ensure no ADA leaves the contract yet
assert(total_ada_in_script >= total_ada_begin, "No ADA can leave the contract");
true
}
}
}
}