Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
228239b
test: malicious upgrade drains funds
romanzac Oct 7, 2025
f7a328d
fix: formatting
romanzac Oct 7, 2025
597181d
test: show success when unauthorized upgrade after malicious
romanzac Oct 8, 2025
e3e3d31
test: offchain proof post lazy erase
romanzac Oct 9, 2025
842e0f1
fix: line length
romanzac Oct 9, 2025
3bce55a
fix: remove offchain lazy erase test - rate limit still applies
romanzac Oct 9, 2025
48ec274
test: timestamp manipulation
romanzac Oct 13, 2025
46fbccb
fix: rename tests
romanzac Oct 13, 2025
e629c8f
Merge branch 'main' into chore-adversarial-tests
romanzac Oct 13, 2025
b2d6cdb
test: front running for registration
romanzac Oct 15, 2025
434c740
fix: unused variables
romanzac Oct 15, 2025
704f017
test: register during spam conditions
romanzac Oct 15, 2025
192fe28
fix: delete failing tests
romanzac Oct 20, 2025
d746ef1
fix: delete MaliciousImplementation
romanzac Oct 20, 2025
499d900
fix: formatting with a new Foundry version
romanzac Oct 20, 2025
38f506c
test: testEraseAndReuse with Echidna
romanzac Oct 24, 2025
199ed30
fix: remove limit check
romanzac Oct 24, 2025
6db28fa
fix: remove test_MultiUserEraseReuseRace
romanzac Oct 27, 2025
0be6279
fix: skip Echidna contract during forge test
romanzac Oct 27, 2025
ab0aed0
test: Echidna contract with invariants
romanzac Oct 27, 2025
f735f68
fix: tune config file
romanzac Oct 27, 2025
d785610
fix: run and cleanup scripts for echidna
romanzac Oct 27, 2025
6541acc
test: Echidna test replay
romanzac Oct 28, 2025
2466d86
fix: Solidity version
romanzac Oct 28, 2025
5a2364a
fix: test_attemptExtensionRace_WakuRLN
romanzac Oct 28, 2025
c4d0e7f
fix: invalid commitment in test_attemptExtensionRace_WakuRLN
romanzac Oct 29, 2025
910f757
fix: invalid commitments in
romanzac Oct 29, 2025
1cd3c91
fix: line length
romanzac Oct 29, 2025
0238995
fix: skip all Echidna tests in CI
romanzac Oct 29, 2025
f5fff5c
chore: fuzz test expansion (#40)
romanzac Nov 7, 2025
ec524dd
test: malicious upgrade drains funds
romanzac Oct 7, 2025
f45228e
fix: formatting
romanzac Oct 7, 2025
0f39a04
test: show success when unauthorized upgrade after malicious
romanzac Oct 8, 2025
aa95aad
test: offchain proof post lazy erase
romanzac Oct 9, 2025
e5867df
fix: line length
romanzac Oct 9, 2025
af846b2
fix: remove offchain lazy erase test - rate limit still applies
romanzac Oct 9, 2025
af5839a
test: timestamp manipulation
romanzac Oct 13, 2025
2463329
fix: rename tests
romanzac Oct 13, 2025
e4a81c9
test: front running for registration
romanzac Oct 15, 2025
f45fb65
fix: unused variables
romanzac Oct 15, 2025
9172659
test: register during spam conditions
romanzac Oct 15, 2025
aefc0ed
fix: delete failing tests
romanzac Oct 20, 2025
7c8d2d7
fix: delete MaliciousImplementation
romanzac Oct 20, 2025
f8d670e
fix: remove test_MultiUserEraseReuseRace
romanzac Oct 27, 2025
fb1053a
test: Echidna test replay
romanzac Oct 28, 2025
4e381c7
fix: Solidity version
romanzac Oct 28, 2025
9fa2355
fix: test_attemptExtensionRace_WakuRLN
romanzac Oct 28, 2025
4ac19d2
fix: invalid commitment in test_attemptExtensionRace_WakuRLN
romanzac Oct 29, 2025
f48a95b
fix: invalid commitments in
romanzac Oct 29, 2025
5d80fab
fix: line length
romanzac Oct 29, 2025
be79a4d
fix: cleanup after rebase
romanzac Nov 7, 2025
fb0fae7
Merge remote-tracking branch 'origin/chore-adversarial-tests' into ch…
romanzac Nov 7, 2025
010f9da
fix: remove redundant file
romanzac Nov 7, 2025
d378171
fix: formatting
romanzac Nov 7, 2025
4de1b76
fix: formatting
romanzac Nov 7, 2025
657c917
fix: adorno + archive EchidnaReplayRaces.t.sol
romanzac Nov 7, 2025
10b89c6
test: focus on erasures with timestamps
romanzac Nov 13, 2025
28d0810
fix: remove isolated test
romanzac Nov 13, 2025
b060b00
test: Echidna tests for races
romanzac Nov 13, 2025
0d161f6
fix: remove unused replay test
romanzac Nov 14, 2025
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
79 changes: 41 additions & 38 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
TestStableTokenTest:test__CannotAddAlreadyMinterRole() (gas: 46286)
TestStableTokenTest:test__CannotMintExceedingMaxSupply() (gas: 26202)
TestStableTokenTest:test__CannotMintWithETHExceedingMaxSupply() (gas: 31164)
TestStableTokenTest:test__CannotMintWithZeroETH() (gas: 18282)
TestStableTokenTest:test__CannotRemoveNonMinterRole() (gas: 22638)
TestStableTokenTest:test__CannotSetMaxSupplyBelowTotalSupply() (gas: 71114)
TestStableTokenTest:test__CheckMinterRoleMapping() (gas: 70651)
TestStableTokenTest:test__ContractDoesNotHoldETHAfterMint() (gas: 110700)
TestStableTokenTest:test__ERC20BasicFunctionality() (gas: 146240)
TestStableTokenTest:test__ETHBurnedEventEmitted() (gas: 112584)
TestStableTokenTest:test__ETHIsBurnedToZeroAddress() (gas: 110545)
TestStableTokenTest:test__MaxSupplyIsSetCorrectly() (gas: 15409)
TestStableTokenTest:test__MintRequiresETH() (gas: 18254)
TestStableTokenTest:test__MintWithDifferentETHAmounts() (gas: 209672)
TestStableTokenTest:test__MinterAddedEventEmitted() (gas: 44991)
TestStableTokenTest:test__MinterRemovedEventEmitted() (gas: 34697)
TestStableTokenTest:test__MinterRoleCanMint() (gas: 98026)
TestStableTokenTest:test__MultipleMinterRolesCanMint() (gas: 128734)
TestStableTokenTest:test__NonMinterNonOwnerAccountCannotMint() (gas: 22444)
TestStableTokenTest:test__NonOwnerCannotAddMinterRole() (gas: 18239)
TestStableTokenTest:test__NonOwnerCannotRemoveMinterRole() (gas: 45775)
TestStableTokenTest:test__NonOwnerCannotSetMaxSupply() (gas: 18070)
TestStableTokenTest:test__OwnerCanAddMinterRole() (gas: 47336)
TestStableTokenTest:test__OwnerCanMintWithoutMinterRole() (gas: 74316)
TestStableTokenTest:test__OwnerCanRemoveMinterRole() (gas: 36544)
TestStableTokenTest:test__OwnerCanSetMaxSupply() (gas: 30683)
TestStableTokenTest:test__RemovedMinterRoleCannotMint() (gas: 37086)
TestStableTokenTest:test__CannotAddAlreadyMinterRole() (gas: 46248)
TestStableTokenTest:test__CannotMintExceedingMaxSupply() (gas: 26253)
TestStableTokenTest:test__CannotMintWithETHExceedingMaxSupply() (gas: 31196)
TestStableTokenTest:test__CannotMintWithZeroETH() (gas: 18269)
TestStableTokenTest:test__CannotRemoveNonMinterRole() (gas: 22686)
TestStableTokenTest:test__CannotSetMaxSupplyBelowTotalSupply() (gas: 71121)
TestStableTokenTest:test__CheckMinterRoleMapping() (gas: 70476)
TestStableTokenTest:test__ContractDoesNotHoldETHAfterMint() (gas: 110659)
TestStableTokenTest:test__ERC20BasicFunctionality() (gas: 146438)
TestStableTokenTest:test__ETHBurnedEventEmitted() (gas: 112577)
TestStableTokenTest:test__ETHIsBurnedToZeroAddress() (gas: 110526)
TestStableTokenTest:test__InitializeZeroReverts() (gas: 2558591)
TestStableTokenTest:test__MaxSupplyIsSetCorrectly() (gas: 15454)
TestStableTokenTest:test__MintRequiresETH() (gas: 18285)
TestStableTokenTest:test__MintWithDifferentETHAmounts() (gas: 209788)
TestStableTokenTest:test__MintWithETH_RevertsBelowOneETH() (gas: 25455)
TestStableTokenTest:test__MintWithETH_SucceedsAtOneETH() (gas: 110063)
TestStableTokenTest:test__MinterAddedEventEmitted() (gas: 44947)
TestStableTokenTest:test__MinterRemovedEventEmitted() (gas: 34662)
TestStableTokenTest:test__MinterRoleCanMint() (gas: 98092)
TestStableTokenTest:test__MultipleMinterRolesCanMint() (gas: 128755)
TestStableTokenTest:test__NonMinterNonOwnerAccountCannotMint() (gas: 22493)
TestStableTokenTest:test__NonOwnerCannotAddMinterRole() (gas: 18223)
TestStableTokenTest:test__NonOwnerCannotRemoveMinterRole() (gas: 45737)
TestStableTokenTest:test__NonOwnerCannotSetMaxSupply() (gas: 18054)
TestStableTokenTest:test__OwnerCanAddMinterRole() (gas: 47248)
TestStableTokenTest:test__OwnerCanMintWithoutMinterRole() (gas: 74382)
TestStableTokenTest:test__OwnerCanRemoveMinterRole() (gas: 36473)
TestStableTokenTest:test__OwnerCanSetMaxSupply() (gas: 30795)
TestStableTokenTest:test__RemovedMinterRoleCannotMint() (gas: 37073)
WakuRlnV2Test:test__EmptyRangePagination() (gas: 307693)
WakuRlnV2Test:test__ErasingNonExistentMembership() (gas: 46131)
WakuRlnV2Test:test__FullCleanUpErasure() (gas: 1016790)
Expand All @@ -44,27 +47,27 @@ WakuRlnV2Test:test__LinearPriceCalculation(uint32) (runs: 1000, μ: 26069, ~: 26
WakuRlnV2Test:test__MassRegistrationAndErasure() (gas: 2714587)
WakuRlnV2Test:test__MaxTotalRateLimitEdgeCases() (gas: 21832122)
WakuRlnV2Test:test__MerkleTreeUpdateAfterErasureAndReuse() (gas: 2426716)
WakuRlnV2Test:test__NonMinterCanMintWithETHAndRegister() (gas: 373178)
WakuRlnV2Test:test__NonMinterCanMintWithETHAndRegister() (gas: 373332)
WakuRlnV2Test:test__OwnerConfigurationUpdates() (gas: 53177)
WakuRlnV2Test:test__PriceCalculatorReconfiguration() (gas: 669789)
WakuRlnV2Test:test__PriceCalculatorReconfiguration() (gas: 669854)
WakuRlnV2Test:test__RegistrationWhenMaxRateLimitIsReached() (gas: 595140)
WakuRlnV2Test:test__ReinitializationProtection() (gas: 80197)
WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1000, μ: 5020828, ~: 2445573)
WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1000, μ: 1146896, ~: 1146896)
WakuRlnV2Test:test__TokenTransferFailures() (gas: 4114224)
WakuRlnV2Test:test__RemoveAllExpiredMemberships(uint32) (runs: 1000, μ: 4516570, ~: 2259520)
WakuRlnV2Test:test__RemoveExpiredMemberships(uint32) (runs: 1000, μ: 1055797, ~: 1055798)
WakuRlnV2Test:test__TokenTransferFailures() (gas: 4139092)
WakuRlnV2Test:test__UnauthorizedMerkleTreeModifications() (gas: 1113852)
WakuRlnV2Test:test__Upgrade() (gas: 6702671)
WakuRlnV2Test:test__UpgradeWithInvalidImplementation() (gas: 51496)
WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1000, μ: 393970, ~: 134452)
WakuRlnV2Test:test__ValidPaginationQuery(uint32) (runs: 1000, μ: 378536, ~: 134452)
WakuRlnV2Test:test__ValidPaginationQuery__OneElement() (gas: 301276)
WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1000, μ: 307585, ~: 307585)
WakuRlnV2Test:test__ValidRegistration(uint32) (runs: 1000, μ: 307650, ~: 307650)
WakuRlnV2Test:test__ValidRegistrationExpiry(uint32) (runs: 1000, μ: 288640, ~: 288640)
WakuRlnV2Test:test__ValidRegistrationExtend(uint32) (runs: 1000, μ: 534996, ~: 534996)
WakuRlnV2Test:test__ValidRegistrationExtendSingleMembership(uint32) (runs: 1000, μ: 296279, ~: 296279)
WakuRlnV2Test:test__ValidRegistrationNoGracePeriod(uint32) (runs: 1000, μ: 292251, ~: 292251)
WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1303567)
WakuRlnV2Test:test__ValidRegistrationWithEraseList() (gas: 1337020)
WakuRlnV2Test:test__ValidRegistration__kats() (gas: 277614)
WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1000, μ: 277708, ~: 277708)
WakuRlnV2Test:test__WithdrawToken(uint32) (runs: 1000, μ: 283285, ~: 283286)
WakuRlnV2Test:test__ZeroGracePeriodDuration() (gas: 8156320)
WakuRlnV2Test:test__ZeroPriceEdgeCase() (gas: 791578)
WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1000, μ: 4235383, ~: 1628509)
WakuRlnV2Test:test__ZeroPriceEdgeCase() (gas: 791643)
WakuRlnV2Test:test__indexReuse_eraseMemberships(uint32) (runs: 1000, μ: 4269053, ~: 1835792)
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ jobs:
)" >> $GITHUB_ENV

- name: "Run the tests"
run: "forge test"
run: 'forge test --no-match-path "test/Echidna*.t.sol"'

- name: "Add test summary"
run: |
Expand Down Expand Up @@ -151,7 +151,7 @@ jobs:
run: "pnpm install"

- name: "Generate the coverage report using the unit and the integration tests"
run: 'forge coverage --match-path "test/**/*.sol" --report lcov'
run: 'forge coverage --match-path "test/**/*.sol" --no-match-path "test/Echidna*.t.sol" --report lcov'

- name: "Upload coverage report to Codecov"
uses: "codecov/codecov-action@v3"
Expand Down
19 changes: 19 additions & 0 deletions echidna.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
solcArgs: "--via-ir --optimize --optimize-runs 1"
testMode: property
testLimit: 100000 # For ~1 hour on strong CPU; adjust
seqLen: 100 # Sequence length for stateful fuzzing
shrinkLimit: 5000
corpusDir: corpus
deployContracts:
- ["0x0000000000000000000000000000000000001000", "PoseidonT3"]
- ["0x0000000000000000000000000000000000001001", "LazyIMT"]
cryticArgs:
- "--compile-libraries=(PoseidonT3,0x0000000000000000000000000000000000001000),(LazyIMT,0x0000000000000000000000000000000000001001)"
propMaxGas: 25000000
testMaxGas: 25000000
maxTimeDelay: 20000000 # ~231 days in seconds; set high to cover active (180 days / 15552000s) and grace (30 days / 2592000s) periods for expiration races
sender: ["0x10000", "0x20000", "0x30000", "0x40000"] # Multiple senders to simulate different users; expand if needed
balanceAddr: 100000000000000000000
coverage: true
quiet: false
projectName: "WakuRlnV2"
5 changes: 5 additions & 0 deletions echidna_cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
rm -rf corpus
rm -f covered*.txt
rm -rf .crytic/
echo "Echidna artifacts cleaned up."
19 changes: 19 additions & 0 deletions echidna_races.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
solcArgs: "--via-ir --optimize --optimize-runs 1"
testMode: assertion
testLimit: 100000 # For ~1 hour on strong CPU; adjust
seqLen: 100 # Sequence length for stateful fuzzing
shrinkLimit: 5000
corpusDir: corpus
deployContracts:
- ["0x0000000000000000000000000000000000001000", "PoseidonT3"]
- ["0x0000000000000000000000000000000000001001", "LazyIMT"]
cryticArgs:
- "--compile-libraries=(PoseidonT3,0x0000000000000000000000000000000000001000),(LazyIMT,0x0000000000000000000000000000000000001001)"
propMaxGas: 25000000
testMaxGas: 25000000
maxTimeDelay: 20000000 # ~231 days in seconds; set high to cover active (180 days / 15552000s) and grace (30 days / 2592000s) periods for expiration races
sender: ["0x10000", "0x20000", "0x30000", "0x40000"] # Multiple senders to simulate different users; expand if needed
balanceAddr: 100000000000000000000
coverage: true
quiet: false
projectName: "WakuRlnV2"
3 changes: 3 additions & 0 deletions run_echidna_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
echidna test/EchidnaTest.t.sol --contract EchidnaTest --config echidna.config.yaml
echidna test/EchidnaTestRaces.t.sol --contract EchidnaTestRaces --config echidna_races.config.yaml
171 changes: 171 additions & 0 deletions test/EchidnaTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "../src/WakuRlnV2.sol";
import "../src/LinearPriceCalculator.sol";
import "../src/Membership.sol";
import "../test/TestStableToken.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract EchidnaTest {
WakuRlnV2 public w;
TestStableToken public token;
LinearPriceCalculator public priceCalculator;

uint32 public constant MAX_TOTAL_RATELIMIT_PER_EPOCH = 160_000;
uint32 public constant MIN_RATELIMIT_PER_MEMBERSHIP = 20;
uint32 public constant MAX_RATELIMIT_PER_MEMBERSHIP = 600;
uint32 public constant ACTIVE_DURATION = 180 days;
uint32 public constant GRACE_PERIOD_DURATION = 30 days;
uint256 public constant MAX_SUPPLY = 1_000_000 * 10 ** 18;

uint256[] public activeIdCommitments;
mapping(uint32 => uint256) public indexToId;
mapping(uint32 => uint32) public indexToRate;
mapping(uint256 => uint32) public idToExpectedActiveDuration;

constructor() {
// Deploy TestStableToken via proxy
address tokenImpl = address(new TestStableToken());
bytes memory tokenInitData = abi.encodeCall(TestStableToken.initialize, (MAX_SUPPLY));
ERC1967Proxy tokenProxy = new ERC1967Proxy(tokenImpl, tokenInitData);
token = TestStableToken(address(tokenProxy));

// Deploy LinearPriceCalculator
priceCalculator = new LinearPriceCalculator(address(token), 0.05 ether);

// Deploy WakuRlnV2 via proxy
address wImpl = address(new WakuRlnV2());
bytes memory wInitData = abi.encodeCall(
WakuRlnV2.initialize,
(
address(priceCalculator),
MAX_TOTAL_RATELIMIT_PER_EPOCH,
MIN_RATELIMIT_PER_MEMBERSHIP,
MAX_RATELIMIT_PER_MEMBERSHIP,
ACTIVE_DURATION,
GRACE_PERIOD_DURATION
)
);
ERC1967Proxy wProxy = new ERC1967Proxy(wImpl, wInitData);
w = WakuRlnV2(address(wProxy));

// Mint and approve tokens
token.mint(address(this), 1_000_000 ether);
token.approve(address(w), type(uint256).max);
}

// Helper for proof verification
function _verifyMerkleProof(
uint256[20] memory proof,
uint256 root,
uint32 index,
uint256 leaf,
uint8 depth
)
internal
pure
returns (bool)
{
uint256 current = leaf;
uint32 idx = index;
for (uint8 level = 0; level < depth; level++) {
bool isLeft = (idx & 1) == 0;
uint256 sibling = proof[level];
uint256[2] memory inputs;
if (isLeft) {
inputs[0] = current;
inputs[1] = sibling;
} else {
inputs[0] = sibling;
inputs[1] = current;
}
current = PoseidonT3.hash(inputs);
idx >>= 1;
}
return current == root;
}

// Invariants

function echidna_rate_commitments_range_correct() public view returns (bool) {
uint32 nextFree = w.nextFreeIndex();
if (nextFree == 0) return true;
uint32 startIndex = 0;
uint32 endIndex = nextFree - 1;
uint256[] memory commitments = w.getRateCommitmentsInRangeBoundsInclusive(startIndex, endIndex);
if (commitments.length != uint256(endIndex - startIndex + 1)) {
return false;
}
for (uint32 j = startIndex; j <= endIndex; j++) {
if (indexToRate[j] != 0) {
uint256 exp = PoseidonT3.hash([indexToId[j], uint256(indexToRate[j])]);
if (commitments[j - startIndex] != exp) {
return false;
}
}
}
return true;
}

function echidna_merkle_proof_valid() public view returns (bool) {
uint32 nextFree = w.nextFreeIndex();
if (nextFree == 0) return true;
for (uint32 index = 0; index < nextFree; index++) {
uint256[20] memory proof = w.getMerkleProof(index);
uint256 root = w.root();
uint256 expectedCommitment = w.getRateCommitmentsInRangeBoundsInclusive(index, index)[0];
if (!_verifyMerkleProof(proof, root, index, expectedCommitment, 20)) {
return false;
}
}
return true;
}

function echidna_total_rate_limit_correct() public view returns (bool) {
uint256 computedTotal = 0;
for (uint256 i = 0; i < activeIdCommitments.length; i++) {
(,,,, uint32 rateLimitMem,,,) = w.memberships(activeIdCommitments[i]);
computedTotal += rateLimitMem;
}
return w.currentTotalRateLimit() == computedTotal;
}

function echidna_max_total_rate_limit_valid() public view returns (bool) {
uint32 maxTotal = w.maxTotalRateLimit();
uint256 currentTotal = w.currentTotalRateLimit();
uint32 maxMembership = w.maxMembershipRateLimit();
return maxTotal >= currentTotal && maxTotal >= maxMembership;
}

function echidna_merkle_inserts_integrity() public view returns (bool) {
uint32 nextFree = w.nextFreeIndex();
if (nextFree == 0) return true;
for (uint32 index = 0; index < nextFree; index++) {
uint256 commitment = w.getRateCommitmentsInRangeBoundsInclusive(index, index)[0];
if (indexToRate[index] != 0) {
uint256 exp = PoseidonT3.hash([indexToId[index], uint256(indexToRate[index])]);
if (commitment != exp) {
return false;
}
}
}
return true;
}

function echidna_merkle_erasures_integrity() public view returns (bool) {
uint32 nextFree = w.nextFreeIndex();
if (nextFree == 0) return true;
for (uint32 index = 0; index < nextFree; index++) {
uint256 commitment = w.getRateCommitmentsInRangeBoundsInclusive(index, index)[0];
if (indexToRate[index] == 0) {
continue; // Erased, skip
}
uint256[20] memory proof = w.getMerkleProof(index);
if (!_verifyMerkleProof(proof, w.root(), index, commitment, 20)) {
return false;
}
}
return true;
}
}
Loading
Loading