Comprehensive Testing for Smart Contracts
module Test.Asset where
import Daml.Script
import Main
-- Test asset creation and transfer
testAssetLifecycle : Script ()
testAssetLifecycle = script do
-- Setup: Allocate parties
issuer <- allocateParty "GoldMint"
alice <- allocateParty "Alice"
bob <- allocateParty "Bob"
-- Act: Create asset with issuer
assetCid <- submit issuer do
createCmd Asset with
issuer = issuer
owner = issuer
description = "1oz Gold Bar"
quantity = 100.0
-- Transfer to Alice (issuer must transfer first)
assetCid <- submit issuer do
exerciseCmd assetCid Transfer with
newOwner = alice
-- Alice transfers to Bob
assetCid <- submit alice do
exerciseCmd assetCid Transfer with
newOwner = bob
-- Assert: Verify final state
Some asset <- queryContractId bob assetCid
asset.owner === bob
asset.quantity === 100.0
-- Test authorization failure
testUnauthorizedTransfer : Script ()
testUnauthorizedTransfer = script do
issuer <- allocateParty "Issuer"
alice <- allocateParty "Alice"
mallory <- allocateParty "Mallory"
assetCid <- submit issuer do
createCmd Asset with
issuer, owner = issuer
description = "Diamond"
quantity = 1.0
assetCid <- submit issuer do
exerciseCmd assetCid Transfer with newOwner = alice
-- This should fail - Mallory can't transfer Alice's asset
submitMustFail mallory do
exerciseCmd assetCid Transfer with newOwner = mallory
-- Test with time
testTimeBasedVesting : Script ()
testTimeBasedVesting = script do
employee <- allocateParty "Employee"
company <- allocateParty "Company"
vestingCid <- submit company do
createCmd VestingSchedule with
company, employee
vestingDate = datetime 2025 Jun 1 0 0 0
-- Before vesting date - should fail
setTime (datetime 2025 May 1 0 0 0)
submitMustFail employee do
exerciseCmd vestingCid Claim
-- After vesting date - should succeed
setTime (datetime 2025 Jul 1 0 0 0)
submit employee do
exerciseCmd vestingCid Claim
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
MockApi, MockQuerier, MockStorage,
};
use cosmwasm_std::{coins, Addr, Timestamp};
fn setup_contract() -> OwnedDeps {
let mut deps = mock_dependencies();
let msg = InstantiateMsg {
admin: "admin".to_string(),
initial_supply: Uint128::new(1000000),
};
let info = mock_info("creator", &coins(1000, "uatom"));
instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
deps
}
#[test]
fn test_transfer_success() {
let mut deps = setup_contract();
// Mint tokens to alice
let info = mock_info("admin", &[]);
let msg = ExecuteMsg::Mint {
recipient: "alice".to_string(),
amount: Uint128::new(100),
};
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
// Alice transfers to bob
let info = mock_info("alice", &[]);
let msg = ExecuteMsg::Transfer {
recipient: "bob".to_string(),
amount: Uint128::new(50),
};
let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap();
// Verify event attributes
assert_eq!(res.attributes[0].value, "transfer");
// Query balances
let alice_balance: BalanceResponse = from_json(
query(
deps.as_ref(),
mock_env(),
QueryMsg::Balance { address: "alice".to_string() },
).unwrap()
).unwrap();
assert_eq!(alice_balance.balance, Uint128::new(50));
let bob_balance: BalanceResponse = from_json(
query(
deps.as_ref(),
mock_env(),
QueryMsg::Balance { address: "bob".to_string() },
).unwrap()
).unwrap();
assert_eq!(bob_balance.balance, Uint128::new(50));
}
#[test]
fn test_unauthorized_transfer() {
let mut deps = setup_contract();
// Try to transfer without balance - should error
let info = mock_info("mallory", &[]);
let msg = ExecuteMsg::Transfer {
recipient: "thief".to_string(),
amount: Uint128::new(100),
};
let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err();
assert!(matches!(err, ContractError::InsufficientFunds { .. }));
}
#[test]
fn test_time_based_vesting() {
let mut deps = setup_contract();
// Create vesting
let info = mock_info("company", &coins(1000, "utoken"));
let vest_time = Timestamp::from_seconds(1735689600); // 2025-01-01
let msg = ExecuteMsg::CreateVesting {
beneficiary: "employee".to_string(),
vest_time,
};
execute(deps.as_mut(), mock_env(), info, msg).unwrap();
// Before vest time - should fail
let mut env = mock_env();
env.block.time = Timestamp::from_seconds(1704067200); // 2024-01-01
let info = mock_info("employee", &[]);
let msg = ExecuteMsg::ClaimVesting {};
let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
assert!(matches!(err, ContractError::VestingNotMature {}));
// After vest time - should succeed
let mut env = mock_env();
env.block.time = Timestamp::from_seconds(1767225600); // 2026-01-01
let info = mock_info("employee", &[]);
let msg = ExecuteMsg::ClaimVesting {};
execute(deps.as_mut(), env, info, msg).unwrap();
}
}
-- Integration test: Complete DVP settlement workflow
testDVPWorkflow : Script ()
testDVPWorkflow = script do
-- Setup market participants
exchange <- allocateParty "Exchange"
bank <- allocateParty "CustodianBank"
buyer <- allocateParty "HedgeFund"
seller <- allocateParty "AssetManager"
-- Create initial positions
-- Seller has securities
securityCid <- submit exchange do
createCmd Security with
exchange
holder = seller
cusip = "912828ZT6" -- Treasury bond
quantity = 1000000
-- Buyer has cash
cashCid <- submit bank do
createCmd CashAccount with
bank
owner = buyer
currency = "USD"
balance = 1050000.0
-- Create DVP instruction
dvpCid <- submit buyer do
createCmd DVPInstruction with
buyer, seller, exchange, bank
securityId = "912828ZT6"
quantity = 1000000
price = 1.02 -- 102% of face value
-- Seller accepts
dvpCid <- submit seller do
exerciseCmd dvpCid AcceptDVP
-- Settlement - atomic delivery vs payment
(newSecCid, newCashCid) <- submitMulti [buyer, seller] [] do
exerciseCmd dvpCid Settle
-- Verify: Securities transferred
Some security <- queryContractId buyer newSecCid
security.holder === buyer
-- Verify: Payment made
Some cash <- queryContractId seller newCashCid
cash.balance === 1020000.0 -- 1M * 1.02
use cw_multi_test::{App, AppBuilder, ContractWrapper, Executor};
fn mock_app() -> App {
AppBuilder::new().build(|router, _, storage| {
router
.bank
.init_balance(
storage,
&Addr::unchecked("buyer"),
coins(1_050_000, "uusd"),
)
.unwrap();
})
}
#[test]
fn test_dvp_workflow() {
let mut app = mock_app();
// Store contracts
let token_code = ContractWrapper::new(
token::execute, token::instantiate, token::query,
);
let token_code_id = app.store_code(Box::new(token_code));
let escrow_code = ContractWrapper::new(
escrow::execute, escrow::instantiate, escrow::query,
);
let escrow_code_id = app.store_code(Box::new(escrow_code));
// Instantiate security token
let security_addr = app
.instantiate_contract(
token_code_id,
Addr::unchecked("exchange"),
&TokenInstantiateMsg {
name: "Treasury".to_string(),
symbol: "TBOND".to_string(),
decimals: 0,
initial_balances: vec![Cw20Coin {
address: "seller".to_string(),
amount: Uint128::new(1_000_000),
}],
},
&[],
"security_token",
None,
)
.unwrap();
// Instantiate escrow for DVP
let escrow_addr = app
.instantiate_contract(
escrow_code_id,
Addr::unchecked("exchange"),
&EscrowInstantiateMsg {
security_token: security_addr.to_string(),
price_denom: "uusd".to_string(),
price_per_unit: Uint128::new(1_020_000),
},
&[],
"dvp_escrow",
None,
)
.unwrap();
// Seller deposits securities
app.execute_contract(
Addr::unchecked("seller"),
security_addr.clone(),
&Cw20ExecuteMsg::Send {
contract: escrow_addr.to_string(),
amount: Uint128::new(1_000_000),
msg: to_json_binary(&EscrowReceiveMsg::DepositSecurity {}).unwrap(),
},
&[],
).unwrap();
// Buyer deposits payment and triggers settlement
app.execute_contract(
Addr::unchecked("buyer"),
escrow_addr.clone(),
&EscrowExecuteMsg::Settle {},
&coins(1_020_000, "uusd"),
).unwrap();
// Verify buyer received securities
let balance: cw20::BalanceResponse = app
.wrap()
.query_wasm_smart(
security_addr,
&Cw20QueryMsg::Balance { address: "buyer".to_string() },
)
.unwrap();
assert_eq!(balance.balance, Uint128::new(1_000_000));
// Verify seller received payment
let seller_balance = app.wrap().query_balance("seller", "uusd").unwrap();
assert_eq!(seller_balance.amount, Uint128::new(1_020_000));
}
Property-based testing verifies invariants hold across many random inputs, catching edge cases traditional tests miss.
-- Property: Total supply is conserved across transfers
propConserveSupply : [Party] -> Script Bool
propConserveSupply parties = script do
issuer <- allocateParty "Issuer"
-- Create initial supply
let totalSupply = 1000000.0
assetCid <- submit issuer do
createCmd Asset with
issuer, owner = issuer
description = "Token"
quantity = totalSupply
-- Perform random transfers
finalAssets <- foldlA (performRandomTransfer issuer) [assetCid] parties
-- Sum all quantities
quantities <- forA finalAssets \cid -> do
Some asset <- queryContractId issuer cid
pure asset.quantity
-- Property: Total equals initial supply
pure (sum quantities == totalSupply)
-- Property: No double-spend possible
propNoDoubleSpend : Script ()
propNoDoubleSpend = script do
alice <- allocateParty "Alice"
bob <- allocateParty "Bob"
carol <- allocateParty "Carol"
issuer <- allocateParty "Issuer"
assetCid <- submit issuer do
createCmd Asset with
issuer, owner = alice
description = "NFT"
quantity = 1.0
-- First transfer succeeds
newCid <- submit alice do
exerciseCmd assetCid Transfer with newOwner = bob
-- Attempting to use archived contract fails
submitMustFail alice do
exerciseCmd assetCid Transfer with newOwner = carol
-- Property: Only owner can transfer
propOwnerControl : Party -> Party -> Script ()
propOwnerControl owner attacker = script do
issuer <- allocateParty "Issuer"
assetCid <- submit issuer do
createCmd Asset with
issuer, owner
description = "Secured"
quantity = 100.0
-- Attacker cannot transfer
submitMustFail attacker do
exerciseCmd assetCid Transfer with newOwner = attacker
use proptest::prelude::*;
proptest! {
// Property: Total supply is conserved
#[test]
fn prop_conserve_supply(
initial_supply in 1u128..1_000_000_000u128,
transfers in prop::collection::vec(
(0usize..10, 0usize..10, 1u128..1000u128),
0..50
)
) {
let mut deps = mock_dependencies();
// Initialize with supply
let msg = InstantiateMsg {
initial_supply: Uint128::new(initial_supply),
};
instantiate(deps.as_mut(), mock_env(), mock_info("admin", &[]), msg).unwrap();
// Distribute to accounts
let accounts: Vec = (0..10).map(|i| format!("user{}", i)).collect();
let per_account = initial_supply / 10;
for acc in &accounts {
let _ = execute(
deps.as_mut(),
mock_env(),
mock_info("admin", &[]),
ExecuteMsg::Mint {
recipient: acc.clone(),
amount: Uint128::new(per_account),
},
);
}
// Perform transfers (may fail, that's ok)
for (from_idx, to_idx, amount) in transfers {
let from = &accounts[from_idx % accounts.len()];
let to = &accounts[to_idx % accounts.len()];
let _ = execute(
deps.as_mut(),
mock_env(),
mock_info(from, &[]),
ExecuteMsg::Transfer {
recipient: to.clone(),
amount: Uint128::new(amount),
},
);
}
// Verify: Sum of all balances equals total supply
let total: u128 = accounts.iter().map(|acc| {
let res: BalanceResponse = from_json(
query(deps.as_ref(), mock_env(), QueryMsg::Balance {
address: acc.clone()
}).unwrap()
).unwrap();
res.balance.u128()
}).sum();
// Admin balance + distributed
let admin_bal: BalanceResponse = from_json(
query(deps.as_ref(), mock_env(), QueryMsg::Balance {
address: "admin".to_string()
}).unwrap()
).unwrap();
prop_assert_eq!(total + admin_bal.balance.u128(), initial_supply);
}
// Property: Unauthorized transfers always fail
#[test]
fn prop_no_unauthorized_transfer(
owner in "[a-z]{5,10}",
attacker in "[a-z]{5,10}",
amount in 1u128..1000u128,
) {
prop_assume!(owner != attacker);
let mut deps = mock_dependencies();
instantiate(
deps.as_mut(),
mock_env(),
mock_info("admin", &[]),
InstantiateMsg { initial_supply: Uint128::new(1000) },
).unwrap();
// Give owner tokens
execute(
deps.as_mut(),
mock_env(),
mock_info("admin", &[]),
ExecuteMsg::Mint {
recipient: owner.clone(),
amount: Uint128::new(amount),
},
).unwrap();
// Attacker cannot transfer owner's tokens
let result = execute(
deps.as_mut(),
mock_env(),
mock_info(&attacker, &[]),
ExecuteMsg::TransferFrom {
owner: owner.clone(),
recipient: attacker.clone(),
amount: Uint128::new(amount),
},
);
prop_assert!(result.is_err());
}
}
name: Daml CI
on: [push, pull_request]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Daml
uses: digital-asset/setup-daml@v2
with:
daml-version: 2.8.0
- name: Build
run: daml build
- name: Run Tests
run: daml test --junit-report test-results.xml
- name: Type Check
run: daml damlc lint
- name: Generate Docs
run: daml damlc docs --output docs
- name: Upload Test Results
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results.xml
integration-test:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Daml
uses: digital-asset/setup-daml@v2
- name: Start Sandbox
run: |
daml sandbox --port 6865 &
sleep 10
- name: Run Integration Scripts
run: |
daml script \
--dar .daml/dist/*.dar \
--script-name Test.Integration:fullWorkflow \
--ledger-host localhost \
--ledger-port 6865
- name: Shutdown
run: pkill -f sandbox || true
name: CosmWasm CI
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
components: clippy, rustfmt
- name: Cache
uses: Swatinem/rust-cache@v2
- name: Format Check
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
- name: Unit Tests
run: cargo test --lib
- name: Integration Tests
run: cargo test --test integration
- name: Build WASM
run: |
cargo build --release --target wasm32-unknown-unknown
mkdir -p artifacts
cp target/wasm32-unknown-unknown/release/*.wasm artifacts/
- name: Check WASM Size
run: |
for wasm in artifacts/*.wasm; do
size=$(stat -f%z "$wasm" 2>/dev/null || stat -c%s "$wasm")
if [ $size -gt 800000 ]; then
echo "WASM too large: $wasm ($size bytes)"
exit 1
fi
done
optimize:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Optimize WASM
run: |
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/optimizer:0.15.0
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: optimized-contracts
path: artifacts/*.wasm
| Testing Aspect | Daml | CosmWasm |
|---|---|---|
| Unit Test Framework | Daml Script (built-in) | Rust #[test] + cosmwasm-std::testing |
| Multi-Party Testing | Native (allocateParty, submit) | Manual (mock_info per call) |
| Authorization Testing | submitMustFail (built-in) | assert!(result.is_err()) |
| Time Manipulation | setTime function | Modify mock_env().block.time |
| Integration Testing | Same scripts against sandbox | cw-multi-test crate |
| Property Testing | QuickCheck-style scripts | proptest crate |
| Coverage Tools | daml test --coverage | cargo tarpaulin / llvm-cov |
| Mocking | Not needed (real ledger) | MockQuerier, mock_dependencies |