diff --git a/.gitmodules b/.gitmodules
index caf1ce87..b6bf3105 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -17,6 +17,9 @@
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
+[submodule "lib/kontrol-cheatcodes"]
+ path = lib/kontrol-cheatcodes
+ url = https://github.com/runtimeverification/kontrol-cheatcodes
[submodule "lib/yearn-vaults-v3"]
path = lib/yearn-vaults-v3
url = https://github.com/yearn/yearn-vaults-v3.git
diff --git a/kontrol.toml b/kontrol.toml
new file mode 100644
index 00000000..2c81847d
--- /dev/null
+++ b/kontrol.toml
@@ -0,0 +1,88 @@
+[build.default]
+foundry-project-root = '.'
+regen = true
+rekompile = true
+verbose = false
+debug = false
+require = 'src/test/kontrol/term-lemmas.k'
+module-import = 'RepoTokenListInvariantsTest:TERM-LEMMAS'
+ast = true
+auxiliary-lemmas = true
+
+[prove.setup]
+foundry-project-root = '.'
+verbose = false
+debug = false
+max-depth = 100000
+max-iterations = 10000
+reinit = false
+cse = false
+workers = 1
+max-frontier-parallel = 6
+maintenance-rate = 24
+assume-defined = true
+no-log-rewrites = true
+kore-rpc-command = 'kore-rpc-booster --no-post-exec-simplify'
+failure-information = true
+counterexample-information = true
+minimize-proofs = false
+fail-fast = true
+smt-timeout = 16000
+smt-retry-limit = 0
+break-every-step = false
+break-on-jumpi = false
+break-on-calls = false
+break-on-storage = false
+break-on-basic-blocks = false
+break-on-cheatcodes = false
+auto_abstract = true
+run-constructor = false
+bmc-depth = 3
+match-test = [
+ "RepoTokenListInvariantsTest.setUp",
+ "TermAuctionListInvariantsTest.setUp",
+ ]
+ast = true
+
+[prove.tests]
+foundry-project-root = '.'
+verbose = false
+debug = false
+max-depth = 100000
+max-iterations = 10000
+reinit = false
+cse = false
+workers = 1
+max-frontier-parallel = 6
+maintenance-rate = 24
+assume-defined = true
+no-log-rewrites = true
+kore-rpc-command = 'kore-rpc-booster --no-post-exec-simplify'
+failure-information = true
+counterexample-information = true
+minimize-proofs = false
+fail-fast = true
+smt-timeout = 16000
+smt-retry-limit = 0
+break-every-step = false
+break-on-jumpi = false
+break-on-calls = false
+break-on-storage = false
+break-on-basic-blocks = false
+break-on-cheatcodes = false
+auto_abstract = true
+run-constructor = false
+match-test = [
+ "RepoTokenListInvariantsTest.testInsertSortedNewToken",
+ "RepoTokenListInvariantsTest.testInsertSortedDuplicateToken",
+ "RepoTokenListInvariantsTest.testRemoveAndRedeemMaturedTokens",
+ "TermAuctionListInvariantsTest.testInsertPendingNewOffer",
+ "TermAuctionListInvariantsTest.testInsertPendingDuplicateOffer",
+ "TermAuctionListInvariantsTest.testRemoveCompleted",
+ ]
+ast = true
+
+[show.default]
+foundry-project-root = '.'
+verbose = true
+debug = false
\ No newline at end of file
diff --git a/lib/kontrol-cheatcodes b/lib/kontrol-cheatcodes
new file mode 160000
index 00000000..c8a62a00
--- /dev/null
+++ b/lib/kontrol-cheatcodes
@@ -0,0 +1 @@
+Subproject commit c8a62a0054532cb9db15a82b6ce25347cccc882c
diff --git a/src/RepoTokenList.sol b/src/RepoTokenList.sol
index 4e0a5670..5d87fb4a 100644
--- a/src/RepoTokenList.sol
+++ b/src/RepoTokenList.sol
@@ -243,7 +243,7 @@ library RepoTokenList {
address prev = current;
while (current != NULL_NODE) {
address next;
- if (getRepoTokenMaturity(current) < block.timestamp) {
+ if (getRepoTokenMaturity(current) <= block.timestamp) {
bool removeMaturedToken;
uint256 repoTokenBalance = ITermRepoToken(current).balanceOf(address(this));
@@ -337,6 +337,7 @@ library RepoTokenList {
* @param repoToken The repoToken to validate and insert
* @param discountRateAdapter The discount rate adapter
* @param asset The address of the base asset
+ * @return validRepoToken Whether the repoToken is valid
* @return discountRate The discount rate to be applied to the validated repoToken
* @return redemptionTimestamp The redemption timestamp of the validated repoToken
*/
@@ -345,14 +346,14 @@ library RepoTokenList {
ITermRepoToken repoToken,
ITermDiscountRateAdapter discountRateAdapter,
address asset
- ) internal returns (uint256 discountRate, uint256 redemptionTimestamp) {
+ ) internal returns (bool validRepoToken, uint256 discountRate, uint256 redemptionTimestamp) {
discountRate = listData.discountRates[address(repoToken)];
if (discountRate != INVALID_AUCTION_RATE) {
(redemptionTimestamp, , ,) = repoToken.config();
// skip matured repoTokens
if (redemptionTimestamp < block.timestamp) {
- revert InvalidRepoToken(address(repoToken));
+ return (false, discountRate, redemptionTimestamp); //revert InvalidRepoToken(address(repoToken));
}
uint256 oracleRate = discountRateAdapter.getDiscountRate(address(repoToken));
@@ -368,11 +369,13 @@ library RepoTokenList {
(isRepoTokenValid, redemptionTimestamp) = validateRepoToken(listData, repoToken, asset);
if (!isRepoTokenValid) {
- revert InvalidRepoToken(address(repoToken));
+ return (false, discountRate, redemptionTimestamp);
}
insertSorted(listData, address(repoToken));
listData.discountRates[address(repoToken)] = discountRate;
}
+
+ return (true, discountRate, redemptionTimestamp);
}
/**
@@ -394,6 +397,8 @@ library RepoTokenList {
return;
}
+ uint256 maturityToInsert = getRepoTokenMaturity(repoToken);
+
address prev;
while (current != NULL_NODE) {
@@ -403,7 +408,6 @@ library RepoTokenList {
}
uint256 currentMaturity = getRepoTokenMaturity(current);
- uint256 maturityToInsert = getRepoTokenMaturity(repoToken);
// Insert repoToken before current if its maturity is less than or equal
if (maturityToInsert < currentMaturity) {
diff --git a/src/Strategy.sol b/src/Strategy.sol
index 41003347..057eb274 100644
--- a/src/Strategy.sol
+++ b/src/Strategy.sol
@@ -999,13 +999,17 @@ contract Strategy is BaseStrategy, Pausable, ReentrancyGuard {
}
// Validate and insert the repoToken into the list, retrieve auction rate and redemption timestamp
- (, uint256 redemptionTimestamp) = repoTokenListData
+ (bool isRepoTokenValid , , uint256 redemptionTimestamp) = repoTokenListData
.validateAndInsertRepoToken(
ITermRepoToken(repoToken),
discountRateAdapter,
address(asset)
);
+ if (!isRepoTokenValid) {
+ revert RepoTokenList.InvalidRepoToken(repoToken);
+ }
+
// Sweep assets and redeem repoTokens, if needed
_redeemRepoTokens(0);
diff --git a/src/TermAuctionList.sol b/src/TermAuctionList.sol
index 1412d0f9..3cdaf430 100644
--- a/src/TermAuctionList.sol
+++ b/src/TermAuctionList.sol
@@ -13,7 +13,7 @@ struct PendingOffer {
address repoToken;
uint256 offerAmount;
ITermAuction termAuction;
- ITermAuctionOfferLocker offerLocker;
+ ITermAuctionOfferLocker offerLocker;
}
struct TermAuctionListNode {
@@ -33,7 +33,7 @@ struct TermAuctionListData {
library TermAuctionList {
using RepoTokenList for RepoTokenListData;
- bytes32 public constant NULL_NODE = bytes32(0);
+ bytes32 public constant NULL_NODE = bytes32(0);
/*//////////////////////////////////////////////////////////////
PRIVATE FUNCTIONS
@@ -84,8 +84,8 @@ library TermAuctionList {
while (current != NULL_NODE) {
offers[i++] = current;
current = _getNext(listData, current);
- }
- }
+ }
+ }
}
/**
@@ -94,11 +94,12 @@ library TermAuctionList {
* @param offerId The ID of the offer to be inserted
* @param pendingOffer The `PendingOffer` struct containing details of the offer to be inserted
*
- * @dev This function inserts a new pending offer while maintaining the list sorted by auction address.
- * The function iterates through the list to find the correct position for the new `offerId` and updates the pointers accordingly.
+ * @dev This function inserts a new pending offer while maintaining the list sorted by auction address.
+ * The function iterates through the list to find the correct position for the new `offerId` and updates the pointers accordingly.
*/
function insertPending(TermAuctionListData storage listData, bytes32 offerId, PendingOffer memory pendingOffer) internal {
bytes32 current = listData.head;
+ require(!pendingOffer.termAuction.auctionCompleted());
// If the list is empty, set the new repoToken as the head
if (current == NULL_NODE) {
@@ -119,7 +120,7 @@ library TermAuctionList {
address currentAuction = address(listData.offers[current].termAuction);
address auctionToInsert = address(pendingOffer.termAuction);
- // Insert repoToken before current if its maturity is less than or equal
+ // Insert offer before current if the auction address to insert is less than current auction address
if (auctionToInsert < currentAuction) {
if (prev == NULL_NODE) {
listData.head = offerId;
@@ -132,7 +133,7 @@ library TermAuctionList {
// Move to the next node
bytes32 next = _getNext(listData, current);
-
+
// If at the end of the list, insert repoToken after current
if (next == NULL_NODE) {
listData.nodes[current].next = offerId;
@@ -158,7 +159,7 @@ library TermAuctionList {
* the list by clearing out inactive offers and ensuring repoTokens are correctly processed.
*/
function removeCompleted(
- TermAuctionListData storage listData,
+ TermAuctionListData storage listData,
RepoTokenListData storage repoTokenListData,
ITermDiscountRateAdapter discountRateAdapter,
address asset
@@ -172,53 +173,51 @@ library TermAuctionList {
PendingOffer memory offer = listData.offers[current];
bytes32 next = _getNext(listData, current);
- uint256 offerAmount = offer.offerLocker.lockedOffer(current).amount;
+ uint256 offerAmount = offer.offerLocker.lockedOfferAmount(current);
bool removeNode;
bool insertRepoToken;
if (offer.termAuction.auctionCompleted()) {
// If auction is completed and closed, mark for removal and prepare to insert repo token
removeNode = true;
- (insertRepoToken, ) = repoTokenListData.validateRepoToken(ITermRepoToken(offer.repoToken), asset);
+ // Auction still open => include offerAmount in totalValue
+ // (otherwise locked purchaseToken will be missing from TV)
+ // Auction completed but not closed => include offer.offerAmount in totalValue
+ // because the offerLocker will have already removed the offer.
+ // This applies if the repoToken hasn't been added to the repoTokenList
+ // (only for new auctions, not reopenings).
+ repoTokenListData.validateAndInsertRepoToken(
+ ITermRepoToken(offer.repoToken), discountRateAdapter, asset
+ );
} else {
- if (offerAmount == 0) {
- // If offer amount is zero, it indicates the auction was canceled or deleted
- removeNode = true;
- } else {
- // Otherwise, do nothing if the offer is still pending
- }
-
if (offer.termAuction.auctionCancelledForWithdrawal()) {
// If auction was canceled for withdrawal, remove the node and unlock offers manually
- removeNode = true;
bytes32[] memory offerIds = new bytes32[](1);
offerIds[0] = current;
- offer.offerLocker.unlockOffers(offerIds); // unlocking offer in this scenario withdraws offer amount
+ try offer.offerLocker.unlockOffers(offerIds) { // unlocking offer in this scenario withdraws offer amount
+ removeNode = true;
+ } catch {
+ removeNode = false;
+ }
+ } else {
+ if (offerAmount == 0) {
+ // If offer amount is zero, it indicates the auction was canceled or deleted
+ removeNode = true;
+ }
}
}
if (removeNode) {
// Update the list to remove the current node
+ delete listData.nodes[current];
+ delete listData.offers[current];
if (current == listData.head) {
listData.head = next;
}
-
- listData.nodes[prev].next = next;
- delete listData.nodes[current];
- delete listData.offers[current];
- }
-
- if (insertRepoToken) {
-
- // Auction still open => include offerAmount in totalValue
- // (otherwise locked purchaseToken will be missing from TV)
- // Auction completed but not closed => include offer.offerAmount in totalValue
- // because the offerLocker will have already removed the offer.
- // This applies if the repoToken hasn't been added to the repoTokenList
- // (only for new auctions, not reopenings).
- repoTokenListData.validateAndInsertRepoToken(
- ITermRepoToken(offer.repoToken), discountRateAdapter, asset
- );
+ else {
+ listData.nodes[prev].next = next;
+ current = prev;
+ }
}
// Move to the next node
@@ -239,10 +238,10 @@ library TermAuctionList {
* @dev This function calculates the present value of offers in the list. If `repoTokenToMatch` is provided,
* it will filter the calculations to include only the specified repoToken. If `repoTokenToMatch` is not provided,
* it will aggregate the present value of all repoTokens in the list. This provides flexibility for both aggregate
- * and specific token evaluations.
+ * and specific token evaluations.
*/
function getPresentValue(
- TermAuctionListData storage listData,
+ TermAuctionListData storage listData,
RepoTokenListData storage repoTokenListData,
ITermDiscountRateAdapter discountRateAdapter,
uint256 purchaseTokenPrecision,
@@ -264,7 +263,7 @@ library TermAuctionList {
continue;
}
- uint256 offerAmount = offer.offerLocker.lockedOffer(current).amount;
+ uint256 offerAmount = offer.offerLocker.lockedOfferAmount(current);
// Handle new or unseen repo tokens
/// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up
@@ -272,15 +271,15 @@ library TermAuctionList {
if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) {
if (edgeCaseAuction != address(offer.termAuction)) {
uint256 repoTokenAmountInBaseAssetPrecision = RepoTokenUtils.getNormalizedRepoTokenAmount(
- offer.repoToken,
+ offer.repoToken,
ITermRepoToken(offer.repoToken).balanceOf(address(this)),
purchaseTokenPrecision,
discountRateAdapter.repoRedemptionHaircut(offer.repoToken)
);
totalValue += RepoTokenUtils.calculatePresentValue(
- repoTokenAmountInBaseAssetPrecision,
- purchaseTokenPrecision,
- RepoTokenList.getRepoTokenMaturity(offer.repoToken),
+ repoTokenAmountInBaseAssetPrecision,
+ purchaseTokenPrecision,
+ RepoTokenList.getRepoTokenMaturity(offer.repoToken),
discountRateAdapter.getDiscountRate(offer.repoToken)
);
@@ -296,7 +295,7 @@ library TermAuctionList {
// Move to the next token in the list
current = _getNext(listData, current);
- }
+ }
}
/**
@@ -305,7 +304,7 @@ library TermAuctionList {
* @param repoTokenListData The repoToken list data
* @param discountRateAdapter The discount rate adapter
* @param repoToken The address of the repoToken (optional)
- * @param newOfferAmount The new offer amount for the specified repoToken
+ * @param newOfferAmount The new offer amount for the specified repoToken
* @param purchaseTokenPrecision The precision of the purchase token
* @return cumulativeWeightedTimeToMaturity The cumulative weighted time to maturity
* @return cumulativeOfferAmount The cumulative repoToken amount
@@ -314,13 +313,13 @@ library TermAuctionList {
* @dev This function calculates cumulative data for all offers in the list. The `repoToken` and `newOfferAmount`
* parameters are optional and provide flexibility to include the newOfferAmount for a specified repoToken in the calculation.
* If `repoToken` is set to `address(0)` or `newOfferAmount` is `0`, the function calculates the cumulative data
- * without adjustments.
+ * without adjustments.
*/
function getCumulativeOfferData(
TermAuctionListData storage listData,
RepoTokenListData storage repoTokenListData,
ITermDiscountRateAdapter discountRateAdapter,
- address repoToken,
+ address repoToken,
uint256 newOfferAmount,
uint256 purchaseTokenPrecision
) internal view returns (uint256 cumulativeWeightedTimeToMaturity, uint256 cumulativeOfferAmount, bool found) {
@@ -339,16 +338,16 @@ library TermAuctionList {
found = true;
} else {
// Retrieve the current offer amount from the offer locker
- offerAmount = offer.offerLocker.lockedOffer(current).amount;
+ offerAmount = offer.offerLocker.lockedOfferAmount(current);
// Handle new repo tokens or reopening auctions
/// @dev offer processed, but auctionClosed not yet called and auction is new so repoToken not on List and wont be picked up
/// checking repoTokendiscountRates to make sure we are not double counting on re-openings
if (repoTokenListData.discountRates[offer.repoToken] == 0 && offer.termAuction.auctionCompleted()) {
// use normalized repoToken amount if repoToken is not in the list
- if (edgeCaseAuction != address(offer.termAuction)) {
+ if (edgeCaseAuction != address(offer.termAuction)) {
offerAmount = RepoTokenUtils.getNormalizedRepoTokenAmount(
- offer.repoToken,
+ offer.repoToken,
ITermRepoToken(offer.repoToken).balanceOf(address(this)),
purchaseTokenPrecision,
discountRateAdapter.repoRedemptionHaircut(offer.repoToken)
@@ -367,12 +366,12 @@ library TermAuctionList {
// Calculate weighted time to maturity
uint256 weightedTimeToMaturity = RepoTokenList.getRepoTokenWeightedTimeToMaturity(
offer.repoToken, offerAmount
- );
+ );
cumulativeWeightedTimeToMaturity += weightedTimeToMaturity;
cumulativeOfferAmount += offerAmount;
}
-
+
// Move to the next token in the list
current = _getNext(listData, current);
}
diff --git a/src/interfaces/term/ITermAuctionOfferLocker.sol b/src/interfaces/term/ITermAuctionOfferLocker.sol
index 64343d3c..208b2404 100644
--- a/src/interfaces/term/ITermAuctionOfferLocker.sol
+++ b/src/interfaces/term/ITermAuctionOfferLocker.sol
@@ -50,6 +50,8 @@ interface ITermAuctionOfferLocker {
function lockedOffer(bytes32 id) external view returns (TermAuctionOffer memory);
+ function lockedOfferAmount(bytes32 id) external view returns (uint256);
+
/// @param offerSubmissions An array of offer submissions
/// @return A bytes32 array of unique on chain offer ids.
function lockOffers(
diff --git a/src/test/kontrol/Constants.sol b/src/test/kontrol/Constants.sol
new file mode 100644
index 00000000..e675d5c6
--- /dev/null
+++ b/src/test/kontrol/Constants.sol
@@ -0,0 +1,4 @@
+pragma solidity 0.8.23;
+
+uint256 constant ETH_UPPER_BOUND = 2 ** 95;
+uint256 constant TIME_UPPER_BOUND = 2 ** 35;
diff --git a/src/test/kontrol/Counterexamples.t.sol b/src/test/kontrol/Counterexamples.t.sol
new file mode 100644
index 00000000..070db6b3
--- /dev/null
+++ b/src/test/kontrol/Counterexamples.t.sol
@@ -0,0 +1,68 @@
+pragma solidity 0.8.23;
+
+import "forge-std/Vm.sol";
+import "forge-std/Test.sol";
+
+import {ERC20Mock} from "@openzeppelin/contracts/mocks/ERC20Mock.sol";
+
+import "src/RepoTokenList.sol";
+
+import {MockTermRepoToken} from "src/test/mocks/MockTermRepoToken.sol";
+import {MockUSDC} from "src/test/mocks/MockUSDC.sol";
+
+/**
+ * Unit tests testing counterexamples for list invariants and other properties
+ * found during formal verification. These tests would fail before the issues
+ * were fixed, but should be passing after.
+ */
+contract CounterexamplesTest is Test {
+ using RepoTokenList for RepoTokenListData;
+
+ RepoTokenListData _listData;
+
+ /**
+ * Scenario: RepoTokenList is repoToken1 -> repoToken2 -> NULL_NODE, where
+ * both tokens have the same maturity. We try to insert repoToken2 again.
+ *
+ * Before: repoToken2 was being inserted before the first token found with the
+ * same maturity, making the list repoToken2 -> repoToken1 -> repoToken2 ->
+ * repoToken1 -> ... and creating a cycle.
+ *
+ * After: A token is inserted only after all other tokens with the same
+ * maturity. Therefore, the function continues past repoToken1 and sees that
+ * repoToken2 is already in the list, avoiding inserting it again.
+ */
+ function testInsertSortedNoCycleCounterexample() external {
+ MockUSDC mockUSDC = new MockUSDC();
+ ERC20Mock mockCollateral = new ERC20Mock();
+
+ // Deploy two repo tokens with the same maturity date
+ MockTermRepoToken repoToken1 = new MockTermRepoToken(
+ bytes32("test repo token 1"), address(mockUSDC), address(mockCollateral), 1e18, block.timestamp + 1 weeks
+ );
+ MockTermRepoToken repoToken2 = new MockTermRepoToken(
+ bytes32("test repo token 2"), address(mockUSDC), address(mockCollateral), 1e18, block.timestamp + 1 weeks
+ );
+
+ // Initialize list to repoToken1 -> repoToken2 -> NULL_NODE
+ _listData.head = address(repoToken1);
+ _listData.nodes[address(repoToken1)].next = address(repoToken2);
+ _listData.nodes[address(repoToken2)].next = RepoTokenList.NULL_NODE;
+
+ // Try to insert repoToken2 again, shouldn't change the list
+ _listData.insertSorted(address(repoToken2));
+
+ address previous = RepoTokenList.NULL_NODE;
+ address current = _listData.head;
+
+ // Check that no next pointers point to the previous node
+ while (current != RepoTokenList.NULL_NODE) {
+ address next = _listData.nodes[current].next;
+
+ assert(next != previous);
+
+ previous = current;
+ current = next;
+ }
+ }
+}
diff --git a/src/test/kontrol/KontrolTest.sol b/src/test/kontrol/KontrolTest.sol
new file mode 100644
index 00000000..1cb57cba
--- /dev/null
+++ b/src/test/kontrol/KontrolTest.sol
@@ -0,0 +1,63 @@
+pragma solidity 0.8.23;
+
+import "forge-std/Vm.sol";
+import "forge-std/Test.sol";
+import "kontrol-cheatcodes/KontrolCheats.sol";
+
+contract KontrolTest is Test, KontrolCheats {
+ // Note: there are lemmas dependent on `ethUpperBound`
+ uint256 constant ethMaxWidth = 96;
+ uint256 constant ethUpperBound = 2 ** ethMaxWidth;
+ // Note: 2 ** 35 takes us to year 3058
+ uint256 constant timeUpperBound = 2 ** 35;
+
+ function infoAssert(bool condition, string memory message) external {
+ if (!condition) {
+ revert(message);
+ }
+ }
+
+ enum Mode {
+ Assume,
+ Try,
+ Assert
+ }
+
+ function _establish(Mode mode, bool condition) internal pure returns (bool) {
+ if (mode == Mode.Assume) {
+ vm.assume(condition);
+ return true;
+ } else if (mode == Mode.Try) {
+ return condition;
+ } else {
+ assert(condition);
+ return true;
+ }
+ }
+
+ function _loadUInt256(address contractAddress, uint256 slot) internal view returns (uint256) {
+ return uint256(vm.load(contractAddress, bytes32(slot)));
+ }
+
+ function _loadAddress(address contractAddress, uint256 slot) internal view returns (address) {
+ return address(uint160(uint256(vm.load(contractAddress, bytes32(slot)))));
+ }
+
+ function _storeBytes32(address contractAddress, uint256 slot, bytes32 value) internal {
+ vm.store(contractAddress, bytes32(slot), value);
+ }
+
+ function _storeUInt256(address contractAddress, uint256 slot, uint256 value) internal {
+ vm.store(contractAddress, bytes32(slot), bytes32(value));
+ }
+
+ function _storeAddress(address contractAddress, uint256 slot, address value) internal {
+ vm.store(contractAddress, bytes32(slot), bytes32(uint256(uint160(value))));
+ }
+
+ function _assumeNoOverflow(uint256 augend, uint256 addend) internal {
+ unchecked {
+ vm.assume(augend < augend + addend);
+ }
+ }
+}
diff --git a/src/test/kontrol/RepoToken.sol b/src/test/kontrol/RepoToken.sol
new file mode 100644
index 00000000..a5d09a85
--- /dev/null
+++ b/src/test/kontrol/RepoToken.sol
@@ -0,0 +1,98 @@
+pragma solidity 0.8.23;
+
+import "forge-std/Test.sol";
+import "kontrol-cheatcodes/KontrolCheats.sol";
+
+import "src/interfaces/term/ITermRepoServicer.sol";
+import "src/interfaces/term/ITermRepoToken.sol";
+
+import "src/test/kontrol/Constants.sol";
+import "src/test/kontrol/TermRepoCollateralManager.sol";
+import "src/test/kontrol/TermRepoServicer.sol";
+
+contract RepoToken is ITermRepoToken, KontrolTest {
+ mapping(address => uint256) _balance;
+ uint256 _redemptionTimestamp;
+ uint256 _redemptionValue;
+ TermRepoServicer _termRepoServicer;
+ TermRepoCollateralManager _termRepoCollateralManager;
+ address _purchaseToken;
+
+ uint256 private constant termRepoServicerSlot = 30;
+ uint256 private constant termRepoCollateralManagerSlot = 31;
+
+ function initializeSymbolic() public {
+ kevm.symbolicStorage(address(this));
+
+ uint256 senderBalance = freshUInt256();
+ vm.assume(senderBalance < ETH_UPPER_BOUND);
+ _balance[msg.sender] = senderBalance;
+
+ _redemptionTimestamp = freshUInt256();
+ vm.assume(_redemptionTimestamp < TIME_UPPER_BOUND);
+
+ _purchaseToken = kevm.freshAddress();
+
+ _redemptionValue = freshUInt256();
+ vm.assume(_redemptionValue < ETH_UPPER_BOUND);
+
+ TermRepoServicer termRepoServicer = new TermRepoServicer();
+ termRepoServicer.initializeSymbolic(address(this));
+ _storeUInt256(address(this), termRepoServicerSlot, uint256(uint160(address(termRepoServicer))));
+
+ TermRepoCollateralManager termRepoCollateralManager = new TermRepoCollateralManager();
+ termRepoCollateralManager.initializeSymbolic();
+ _storeUInt256(address(this), termRepoCollateralManagerSlot, uint256(uint160(address(termRepoCollateralManager))));
+ }
+
+ function decimals() public view returns (uint8) {
+ return 18;
+ }
+
+ function balanceOf(address account) public view returns (uint256) {
+ return _balance[account];
+ }
+
+ function redemptionValue() external view returns (uint256) {
+ return _redemptionValue;
+ }
+
+ function config() external view returns (
+ uint256 redemptionTimestamp,
+ address purchaseToken,
+ address termRepoServicer,
+ address termRepoCollateralManager
+ ) {
+ redemptionTimestamp = _redemptionTimestamp;
+ purchaseToken = _purchaseToken;
+ termRepoServicer = address(_termRepoServicer);
+ termRepoCollateralManager = address(_termRepoCollateralManager);
+ }
+
+ function termRepoId() external view returns (bytes32) {
+ return bytes32(freshUInt256());
+ }
+
+ function allowance(address owner, address spender) external view returns (uint256) {
+ return freshUInt256();
+ }
+
+ function approve(address spender, uint256 amount) external returns (bool) {
+ kevm.symbolicStorage(address(this));
+ return kevm.freshBool() > 0;
+ }
+
+ function totalSupply() external view returns (uint256) {
+ return freshUInt256();
+ }
+
+ function transfer(address to, uint256 amount) external returns (bool) {
+ kevm.symbolicStorage(address(this));
+ return kevm.freshBool() > 0;
+ }
+
+ function transferFrom(address from, address to, uint256 amount) external returns (bool) {
+ kevm.symbolicStorage(address(this));
+ return kevm.freshBool() > 0;
+ }
+}
diff --git a/src/test/kontrol/RepoTokenListInvariants.t.sol b/src/test/kontrol/RepoTokenListInvariants.t.sol
new file mode 100644
index 00000000..d753dc28
--- /dev/null
+++ b/src/test/kontrol/RepoTokenListInvariants.t.sol
@@ -0,0 +1,328 @@
+pragma solidity 0.8.23;
+
+import "forge-std/Vm.sol";
+import "forge-std/Test.sol";
+import "kontrol-cheatcodes/KontrolCheats.sol";
+
+import "src/RepoTokenList.sol";
+
+import "src/test/kontrol/Constants.sol";
+import "src/test/kontrol/RepoToken.sol";
+
+contract RepoTokenListInvariantsTest is KontrolTest {
+ using RepoTokenList for RepoTokenListData;
+
+ RepoTokenListData _repoTokenList;
+
+ function setUp() public {
+ // Make storage of this contract completely symbolic
+ kevm.symbolicStorage(address(this));
+
+ // Initialize RepoTokenList of arbitrary size
+ _initializeRepoTokenList();
+ }
+
+ /**
+ * Deploy a new RepoToken with symbolic storage.
+ */
+ function _newRepoToken() internal returns (address) {
+ RepoToken repoToken = new RepoToken();
+ repoToken.initializeSymbolic();
+
+ return address(repoToken);
+ }
+
+ /**
+ * Return the maturity timestamp of the given RepoToken.
+ */
+ function _getRepoTokenMaturity(address repoToken) internal returns (uint256 redemptionTimestamp) {
+ (redemptionTimestamp, , ,) = ITermRepoToken(repoToken).config();
+ }
+
+ /**
+ * Return the this contract's balance in the given RepoToken.
+ */
+ function _getRepoTokenBalance(address repoToken) internal returns (uint256) {
+ return ITermRepoToken(repoToken).balanceOf(address(this));
+ }
+
+ /**
+ * Initialize _repoTokenList to a RepoTokenList of arbitrary size, where all
+ * items are distinct RepoTokens with symbolic storage.
+ */
+ function _initializeRepoTokenList() internal {
+ address previous = RepoTokenList.NULL_NODE;
+
+ while (kevm.freshBool() != 0) {
+ address current = _newRepoToken();
+
+ if (previous == RepoTokenList.NULL_NODE) {
+ _repoTokenList.head = current;
+ } else {
+ _repoTokenList.nodes[previous].next = current;
+ }
+
+ previous = current;
+ }
+
+ if (previous == RepoTokenList.NULL_NODE) {
+ _repoTokenList.head = RepoTokenList.NULL_NODE;
+ } else {
+ _repoTokenList.nodes[previous].next = RepoTokenList.NULL_NODE;
+ }
+ }
+
+ /**
+ * Assume or assert that the tokens in the list are sorted by maturity.
+ */
+ function _establishSortedByMaturity(Mode mode) internal {
+ address previous = RepoTokenList.NULL_NODE;
+ address current = _repoTokenList.head;
+
+ while (current != RepoTokenList.NULL_NODE) {
+ if (previous != RepoTokenList.NULL_NODE) {
+ uint256 previousMaturity = _getRepoTokenMaturity(previous);
+ uint256 currentMaturity = _getRepoTokenMaturity(current);
+ _establish(mode, previousMaturity <= currentMaturity);
+ }
+
+ previous = current;
+ current = _repoTokenList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume or assert that there are no duplicate tokens in the list.
+ */
+ function _establishNoDuplicateTokens(Mode mode) internal {
+ address current = _repoTokenList.head;
+
+ while (current != RepoTokenList.NULL_NODE) {
+ address other = _repoTokenList.nodes[current].next;
+
+ while (other != RepoTokenList.NULL_NODE) {
+ _establish(mode, current != other);
+ other = _repoTokenList.nodes[other].next;
+ }
+
+ current = _repoTokenList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume or assert that there are no tokens in the list have matured
+ * (i.e. all token maturities are greater than the current timestamp).
+ */
+ function _establishNoMaturedTokens(Mode mode) internal {
+ address current = _repoTokenList.head;
+
+ while (current != RepoTokenList.NULL_NODE) {
+ uint256 currentMaturity = _getRepoTokenMaturity(current);
+
+ _establish(mode, block.timestamp < currentMaturity);
+
+ current = _repoTokenList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume or assert that all tokens in the list have balance > 0.
+ */
+ function _establishPositiveBalance(Mode mode) internal {
+ address current = _repoTokenList.head;
+
+ while (current != RepoTokenList.NULL_NODE) {
+ uint256 repoTokenBalance = _getRepoTokenBalance(current);
+
+ _establish(mode, 0 < repoTokenBalance);
+
+ current = _repoTokenList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Weaker version of the above invariant that allows matured tokens to have
+ * a balance of 0.
+ *
+ * Note: This is equivalent to the above invariant if the NoMaturedTokens
+ * invariant also holds.
+ */
+ function _establishPositiveBalanceForNonMaturedTokens(Mode mode) internal {
+ address current = _repoTokenList.head;
+
+ while (current != RepoTokenList.NULL_NODE) {
+ uint256 currentMaturity = _getRepoTokenMaturity(current);
+ uint256 repoTokenBalance = _getRepoTokenBalance(current);
+
+ if (block.timestamp < currentMaturity) {
+ _establish(mode, 0 < repoTokenBalance);
+ }
+
+ current = _repoTokenList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Count the number of nodes in the list.
+ *
+ * Note that this function guarantees the following postconditions:
+ * - The head of the list is NULL_NODE iff the count is 0.
+ * - If the count is N, the Nth node in the list is followed by NULL_NODE.
+ */
+ function _countNodesInList() internal returns (uint256) {
+ uint256 count = 0;
+ address current = _repoTokenList.head;
+
+ while (current != RepoTokenList.NULL_NODE) {
+ ++count;
+ current = _repoTokenList.nodes[current].next;
+ }
+
+ return count;
+ }
+
+ /**
+ * Return true if the given RepoToken is in the list, and false otherwise.
+ */
+ function _repoTokenInList(address repoToken) internal returns (bool) {
+ address current = _repoTokenList.head;
+
+ while (current != RepoTokenList.NULL_NODE) {
+ if (current == repoToken) {
+ return true;
+ }
+
+ current = _repoTokenList.nodes[current].next;
+ }
+
+ return false;
+ }
+
+ /**
+ * Test that insertSorted preserves the list invariants when a new RepoToken
+ * is added (that was not present in the list before).
+ */
+ function testInsertSortedNewToken() external {
+ // Our initialization procedure guarantees this invariant,
+ // so we assert instead of assuming
+ _establishNoDuplicateTokens(Mode.Assert);
+
+ // Assume that the invariants are satisfied before the function is called
+ _establishSortedByMaturity(Mode.Assume);
+ _establishNoMaturedTokens(Mode.Assume);
+ _establishPositiveBalance(Mode.Assume);
+
+ // Save the number of tokens in the list before the function is called
+ uint256 count = _countNodesInList();
+
+ // Generate a new RepoToken with symbolic storage
+ address repoToken = _newRepoToken();
+ uint256 maturity = _getRepoTokenMaturity(repoToken);
+ uint256 balance = _getRepoTokenBalance(repoToken);
+ vm.assume(block.timestamp < maturity);
+ vm.assume(0 < balance);
+
+ // Call the function being tested
+ _repoTokenList.insertSorted(repoToken);
+
+
+ // Assert that the size of the list increased by 1
+ assert(_countNodesInList() == count + 1);
+
+ // Assert that the new RepoToken is in the list
+ assert(_repoTokenInList(repoToken));
+
+ // Assert that the invariants are preserved
+ _establishSortedByMaturity(Mode.Assert);
+ _establishNoDuplicateTokens(Mode.Assert);
+ _establishNoMaturedTokens(Mode.Assert);
+ _establishPositiveBalance(Mode.Assert);
+ }
+
+ /**
+ * Test that insertSorted preserves the list invariants when trying to
+ * insert a RepoToken that is already in the list.
+ */
+ function testInsertSortedDuplicateToken(
+ address repoToken
+ ) external {
+ // Our initialization procedure guarantees this invariant,
+ // so we assert instead of assuming
+ _establishNoDuplicateTokens(Mode.Assert);
+
+ // Assume that the invariants are satisfied before the function is called
+ _establishSortedByMaturity(Mode.Assume);
+ _establishNoMaturedTokens(Mode.Assume);
+ _establishPositiveBalance(Mode.Assume);
+
+ // Save the number of tokens in the list before the function is called
+ uint256 count = _countNodesInList();
+
+ // Assume that the RepoToken is already in the list
+ vm.assume(_repoTokenInList(repoToken));
+
+ // Call the function being tested
+ _repoTokenList.insertSorted(repoToken);
+
+ // Assert that the size of the list didn't change
+ assert(_countNodesInList() == count);
+
+ // Assert that the RepoToken is still in the list
+ assert(_repoTokenInList(repoToken));
+
+ // Assert that the invariants are preserved
+ _establishSortedByMaturity(Mode.Assert);
+ _establishNoDuplicateTokens(Mode.Assert);
+ _establishNoMaturedTokens(Mode.Assert);
+ _establishPositiveBalance(Mode.Assert);
+ }
+
+ /**
+ * Configure the model of the RepoServicer for every token in the list to
+ * follow the assumption that redeemTermRepoTokens will not revert.
+ */
+ function _guaranteeRedeemAlwaysSucceeds() internal {
+ address current = _repoTokenList.head;
+
+ while (current != RepoTokenList.NULL_NODE) {
+ (, , address repoServicer,) = ITermRepoToken(current).config();
+ TermRepoServicer(repoServicer).guaranteeRedeemAlwaysSucceeds();
+
+ current = _repoTokenList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Test that removeAndRedeemMaturedTokens preserves the list invariants.
+ */
+ function testRemoveAndRedeemMaturedTokens() external {
+ // Save the number of tokens in the list before the function is called
+ uint256 count = _countNodesInList();
+
+ // Our initialization procedure guarantees this invariant,
+ // so we assert instead of assuming
+ _establishNoDuplicateTokens(Mode.Assert);
+
+ // Assume that the invariants are satisfied before the function is called
+ _establishSortedByMaturity(Mode.Assume);
+ _establishNoDuplicateTokens(Mode.Assume);
+ _establishPositiveBalanceForNonMaturedTokens(Mode.Assume);
+
+ // Assume that the call to redeemTermRepoTokens will not revert
+ _guaranteeRedeemAlwaysSucceeds();
+
+ // Call the function being tested
+ _repoTokenList.removeAndRedeemMaturedTokens();
+
+ // Assert that the size of the list is less than or equal to before
+ assert(_countNodesInList() <= count);
+
+ // Assert that the invariants are preserved
+ _establishSortedByMaturity(Mode.Assert);
+ _establishNoDuplicateTokens(Mode.Assert);
+
+ // Now the following invariants should hold as well
+ _establishNoMaturedTokens(Mode.Assert);
+ _establishPositiveBalance(Mode.Assert);
+ }
+}
diff --git a/src/test/kontrol/TermAuction.sol b/src/test/kontrol/TermAuction.sol
new file mode 100644
index 00000000..ed38062b
--- /dev/null
+++ b/src/test/kontrol/TermAuction.sol
@@ -0,0 +1,41 @@
+pragma solidity 0.8.23;
+
+import "src/interfaces/term/ITermAuction.sol";
+
+import "src/test/kontrol/Constants.sol";
+import "src/test/kontrol/KontrolTest.sol";
+
+contract TermAuction is ITermAuction, KontrolTest {
+ bool _auctionCompleted;
+ bool _auctionCancelledForWithdrawal;
+
+ uint256 private constant auctionSlot = 27;
+
+ function initializeSymbolic() public {
+ kevm.symbolicStorage(address(this));
+ // Clear the slot that holds two contract fields
+ _storeUInt256(address(this), auctionSlot, 0);
+ _auctionCompleted = kevm.freshBool() != 0;
+ _auctionCancelledForWithdrawal = kevm.freshBool() != 0;
+ }
+
+ function termAuctionOfferLocker() external view returns (address) {
+ return kevm.freshAddress();
+ }
+
+ function termRepoId() external view returns (bytes32) {
+ return bytes32(freshUInt256());
+ }
+
+ function auctionEndTime() external view returns (uint256) {
+ return freshUInt256();
+ }
+
+ function auctionCompleted() external view returns (bool) {
+ return _auctionCompleted;
+ }
+
+ function auctionCancelledForWithdrawal() external view returns (bool) {
+ return _auctionCancelledForWithdrawal;
+ }
+}
diff --git a/src/test/kontrol/TermAuctionListInvariants.t.sol b/src/test/kontrol/TermAuctionListInvariants.t.sol
new file mode 100644
index 00000000..5df500ff
--- /dev/null
+++ b/src/test/kontrol/TermAuctionListInvariants.t.sol
@@ -0,0 +1,552 @@
+pragma solidity 0.8.23;
+
+import "forge-std/Vm.sol";
+import "forge-std/Test.sol";
+import "kontrol-cheatcodes/KontrolCheats.sol";
+
+import "src/RepoTokenList.sol";
+import "src/TermAuctionList.sol";
+
+import "src/test/kontrol/Constants.sol";
+import "src/test/kontrol/RepoToken.sol";
+import "src/test/kontrol/RepoTokenListInvariants.t.sol";
+import "src/test/kontrol/TermAuction.sol";
+import "src/test/kontrol/TermAuctionOfferLocker.sol";
+import "src/test/kontrol/TermDiscountRateAdapter.sol";
+
+contract TermAuctionListInvariantsTest is KontrolTest {
+ using TermAuctionList for TermAuctionListData;
+
+ TermAuctionListData _termAuctionList;
+ address _referenceAuction;
+ RepoTokenListData _repoTokenList;
+
+ uint256 private constant auctionListSlot = 27;
+ uint256 private constant referenceAuctionSlot = 30;
+
+ function setUp() public {
+ // Make storage of this contract completely symbolic
+ kevm.symbolicStorage(address(this));
+
+ // We will copy the code of this deployed auction contract
+ // into all auctions in the list
+ _storeUInt256(address(this), referenceAuctionSlot, uint256(uint160(address(new TermAuction()))));
+
+ // Initialize TermAuctionList of arbitrary size
+ _initializeTermAuctionList();
+ }
+
+ function auctionListOfferSlot(bytes32 offerId) internal view returns (uint256) {
+ return uint256(keccak256(abi.encodePacked(uint256(offerId), uint256(auctionListSlot + 2))));
+ }
+
+ /**
+ * Set pending offer using slot manipulation directly
+ */
+ function setPendingOffer(bytes32 offerId, address repoToken, uint256 offerAmount, address auction, address offerLocker) internal {
+ uint256 offerSlot = auctionListOfferSlot(offerId);
+ _storeUInt256(address(this), offerSlot, uint256(uint160(repoToken)));
+ _storeUInt256(address(this), offerSlot + 1, offerAmount);
+ _storeUInt256(address(this), offerSlot + 2, uint256(uint160(auction)));
+ _storeUInt256(address(this), offerSlot + 3, uint256(uint160(offerLocker)));
+ }
+
+ /**
+ * Return the auction for a given offer in the list.
+ */
+ function _getAuction(bytes32 offerId) internal returns(address) {
+ return address(_termAuctionList.offers[offerId].termAuction);
+ }
+
+ /**
+ * Deploy & initialize RepoToken and OfferLocker with the same RepoServicer
+ */
+ function newRepoTokenAndOfferLocker() public returns (
+ RepoToken repoToken,
+ TermAuctionOfferLocker offerLocker
+ ) {
+ repoToken = new RepoToken();
+ repoToken.initializeSymbolic();
+ (, , address termRepoServicer,) = repoToken.config();
+
+ offerLocker = new TermAuctionOfferLocker();
+ offerLocker.initializeSymbolic(termRepoServicer);
+ }
+
+ /**
+ * Initialize _termAuctionList to a TermAuctionList of arbitrary size,
+ * comprised of offers with distinct ids.
+ */
+ function _initializeTermAuctionList() internal {
+ bytes32 previous = TermAuctionList.NULL_NODE;
+ uint256 count = 0;
+
+ while (kevm.freshBool() != 0) {
+ (RepoToken repoToken, TermAuctionOfferLocker offerLocker) =
+ this.newRepoTokenAndOfferLocker();
+
+ // Assign each offer an ID based on Strategy._generateOfferId()
+ bytes32 current = keccak256(
+ abi.encodePacked(count, address(this), address(offerLocker))
+ );
+ // Register offer in offer locker
+ offerLocker.initializeSymbolicLockedOfferFor(current);
+
+ if (previous == TermAuctionList.NULL_NODE) {
+ _termAuctionList.head = current;
+ } else {
+ _termAuctionList.nodes[previous].next = current;
+ }
+
+ // Create sequential addresses to ensure that list is sorted
+ address auction = address(uint160(1000 + 2 * count));
+ // Etch the code of the auction contract into this address
+ this.etch(auction, _referenceAuction);
+ TermAuction(auction).initializeSymbolic();
+
+ // Build PendingOffer
+ setPendingOffer(current, address(repoToken), freshUInt256(), auction, address(offerLocker));
+
+ previous = current;
+ ++count;
+ }
+
+ if (previous == TermAuctionList.NULL_NODE) {
+ _termAuctionList.head = TermAuctionList.NULL_NODE;
+ } else {
+ _termAuctionList.nodes[previous].next = TermAuctionList.NULL_NODE;
+ }
+ }
+
+ /**
+ * Initialize the TermDiscountRateAdapter to a symbolic state, ensuring that
+ * it has a symbolic discount rate for every token in the PendingOffers.
+ */
+ function _initializeDiscountRateAdapter(
+ TermDiscountRateAdapter discountRateAdapter
+ ) internal {
+ discountRateAdapter.initializeSymbolic();
+
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ address repoToken = _termAuctionList.offers[current].repoToken;
+ discountRateAdapter.initializeSymbolicParamsFor(repoToken);
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume or assert that the offers in the list are sorted by auction.
+ */
+ function _establishSortedByAuctionId(Mode mode) internal {
+ bytes32 previous = TermAuctionList.NULL_NODE;
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ if (previous != TermAuctionList.NULL_NODE) {
+ address previousAuction = _getAuction(previous);
+ address currentAuction = _getAuction(current);
+ _establish(mode, previousAuction <= currentAuction);
+ }
+
+ previous = current;
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume or assert that there are no duplicate offers in the list.
+ */
+ function _establishNoDuplicateOffers(Mode mode) internal {
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ bytes32 other = _termAuctionList.nodes[current].next;
+
+ while (other != TermAuctionList.NULL_NODE) {
+ _establish(mode, current != other);
+ other = _termAuctionList.nodes[other].next;
+ }
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume or assert that there are no completed auctions in the list.
+ */
+ function _establishNoCompletedAuctions(Mode mode) internal {
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ PendingOffer storage offer = _termAuctionList.offers[current];
+ _establish(mode, !offer.termAuction.auctionCompleted());
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume or assert that all offer amounts are > 0.
+ */
+ function _establishPositiveOfferAmounts(Mode mode) internal {
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ PendingOffer storage offer = _termAuctionList.offers[current];
+ _establish(mode, 0 < offer.offerAmount);
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume or assert that the offer amounts recorded in the list are the same
+ * as the offer amounts in the offer locker.
+ */
+ function _establishOfferAmountMatchesAmountLocked(Mode mode, bytes32 offerId) internal {
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ if(offerId == 0 || offerId != current) {
+ PendingOffer storage offer = _termAuctionList.offers[current];
+ uint256 offerAmount = offer.offerLocker.lockedOfferAmount(current);
+ _establish(mode, offer.offerAmount == offerAmount);
+ }
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Count the number of offers in the list.
+ *
+ * Note that this function guarantees the following postconditions:
+ * - The head of the list is NULL_NODE iff the count is 0.
+ * - If the count is N, the Nth node in the list is followed by NULL_NODE.
+ */
+ function _countOffersInList() internal returns (uint256) {
+ uint256 count = 0;
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ ++count;
+ current = _termAuctionList.nodes[current].next;
+ }
+
+ return count;
+ }
+
+ /**
+ * Return true if the given offer id is in the list, and false otherwise.
+ */
+ function _offerInList(bytes32 offerId) internal returns (bool) {
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ if (current == offerId) {
+ return true;
+ }
+
+ current = _termAuctionList.nodes[current].next;
+ }
+
+ return false;
+ }
+
+ /**
+ * Assume that the address doesn't overlap with any preexisting addresses.
+ * This is necessary in order to use cheatcodes on a symbolic address that
+ * change its code or storage.
+ */
+ function _assumeNewAddress(address freshAddress) internal {
+ vm.assume(10 <= uint160(freshAddress));
+
+ vm.assume(freshAddress != address(this));
+ vm.assume(freshAddress != address(vm));
+ vm.assume(freshAddress != address(kevm));
+
+ vm.assume(freshAddress != address(_referenceAuction));
+
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ PendingOffer storage offer = _termAuctionList.offers[current];
+ (,, address termRepoServicer, address termRepoCollateralManager) =
+ ITermRepoToken(offer.repoToken).config();
+
+ vm.assume(freshAddress != offer.repoToken);
+ vm.assume(freshAddress != address(offer.termAuction));
+ vm.assume(freshAddress != address(offer.offerLocker));
+ vm.assume(freshAddress != termRepoServicer);
+ vm.assume(freshAddress != termRepoCollateralManager);
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Etch the code at a given address to a given address in an external call,
+ * reducing memory consumption in the caller function
+ */
+ function etch(address dest, address src) public {
+ vm.etch(dest, src.code);
+ }
+
+ /**
+ * Test that insertPending preserves the list invariants when a new offer
+ * is added (that was not present in the list before).
+ */
+ function testInsertPendingNewOffer(
+ bytes32 offerId
+ ) external {
+ // offerId must not equal zero, otherwise the linked list breaks
+ // TODO: Does the code protect against this?
+ vm.assume(offerId != TermAuctionList.NULL_NODE);
+
+ // Our initialization procedure guarantees these invariants,
+ // so we assert instead of assuming
+ _establishSortedByAuctionId(Mode.Assert);
+ _establishNoDuplicateOffers(Mode.Assert);
+
+ // Assume that the invariants hold before the function is called
+ _establishOfferAmountMatchesAmountLocked(Mode.Assume, bytes32(0));
+ _establishNoCompletedAuctions(Mode.Assume);
+ _establishPositiveOfferAmounts(Mode.Assume);
+
+ // Save the number of offers in the list before the function is called
+ uint256 count = _countOffersInList();
+
+ // Assume that the auction is a fresh address that doesn't overlap with
+ // any others, then initialize it to contain TermAuction code
+ //
+ // NOTE: The auction address needs to remain symbolic, otherwise its
+ // place in the list will be predetermined and the test won't be general
+ address auction = freshAddress();
+ _assumeNewAddress(auction);
+
+ // Initialize RepoToken and OfferLocker, making sure that the addresses
+ // also don't overlap with the symbolic auction
+ (RepoToken repoToken, TermAuctionOfferLocker offerLocker) =
+ this.newRepoTokenAndOfferLocker();
+ offerLocker.initializeSymbolicLockedOfferFor(offerId);
+ (,, address termRepoServicer, address termRepoCollateralManager) =
+ repoToken.config();
+ vm.assume(0 < offerLocker.lockedOfferAmount(offerId));
+ vm.assume(auction != address(repoToken));
+ vm.assume(auction != address(offerLocker));
+ vm.assume(auction != termRepoServicer);
+ vm.assume(auction != termRepoCollateralManager);
+
+ // Now we can etch the auction in, when all other addresses have been created
+ this.etch(auction, _referenceAuction);
+ TermAuction(auction).initializeSymbolic();
+ vm.assume(!TermAuction(auction).auctionCompleted());
+
+ // Build new PendingOffer
+ PendingOffer memory pendingOffer;
+ pendingOffer.repoToken = address(repoToken);
+ pendingOffer.offerAmount = offerLocker.lockedOfferAmount(offerId);
+ pendingOffer.termAuction = ITermAuction(auction);
+ pendingOffer.offerLocker = ITermAuctionOfferLocker(offerLocker);
+
+ // Assume that the offer is not already in the list
+ vm.assume(!_offerInList(offerId));
+
+ // Call the function being tested
+ _termAuctionList.insertPending(offerId, pendingOffer);
+
+ // Assert that the size of the list increased by 1
+ // NOTE: This assertion breaks if offerId equals zero
+ assert(_countOffersInList() == count + 1);
+
+ // Assert that the new offer is in the list
+ assert(_offerInList(offerId));
+
+ // Assert that the invariants are preserved
+ _establishSortedByAuctionId(Mode.Assert);
+ _establishNoDuplicateOffers(Mode.Assert);
+ _establishNoCompletedAuctions(Mode.Assert);
+ _establishPositiveOfferAmounts(Mode.Assert);
+ _establishOfferAmountMatchesAmountLocked(Mode.Assert, bytes32(0));
+ }
+
+
+ /**
+ * Test that insertPending preserves the list invariants when trying to
+ * insert an offer that is already in the list.
+ */
+ function testInsertPendingDuplicateOffer(
+ bytes32 offerId,
+ PendingOffer memory pendingOffer
+ ) external {
+ // offerId must not equal zero, otherwise the linked list breaks
+ // TODO: Does the code protect against this?
+ vm.assume(offerId != TermAuctionList.NULL_NODE);
+
+ // Save the number of offers in the list before the function is called
+ uint256 count = _countOffersInList();
+
+ // Assume that the offer is already in the list
+ vm.assume(_offerInList(offerId));
+
+ // Our initialization procedure guarantees these invariants,
+ // so we assert instead of assuming
+ _establishSortedByAuctionId(Mode.Assert);
+ _establishNoDuplicateOffers(Mode.Assert);
+
+ // Assume that the invariants hold before the function is called
+ _establishOfferAmountMatchesAmountLocked(Mode.Assume, offerId);
+ _establishNoCompletedAuctions(Mode.Assume);
+ _establishPositiveOfferAmounts(Mode.Assume);
+
+ PendingOffer memory offer = _termAuctionList.offers[offerId];
+ // Calls to the Strategy.submitAuctionOffer need to ensure that the following 2 assumptions hold before the call
+ vm.assume(offer.termAuction == pendingOffer.termAuction);
+ vm.assume(offer.repoToken == address(pendingOffer.repoToken));
+ // This is ensured by the _validateAndGetOfferLocker if the above assumptions hold
+ vm.assume(offer.offerLocker == pendingOffer.offerLocker);
+ // This is being checked by Strategy.submitAuctionOffer
+ vm.assume(pendingOffer.offerAmount > 0);
+ vm.assume(pendingOffer.offerAmount == pendingOffer.offerLocker.lockedOfferAmount(offerId));
+
+ // Call the function being tested
+ _termAuctionList.insertPending(offerId, pendingOffer);
+
+ // Assert that the size of the list didn't change
+ assert(_countOffersInList() == count);
+
+ // Assert that the new offer is in the list
+ assert(_offerInList(offerId));
+
+ // Assert that the invariants are preserved
+ _establishSortedByAuctionId(Mode.Assert);
+ _establishNoDuplicateOffers(Mode.Assert);
+ _establishNoCompletedAuctions(Mode.Assert);
+ _establishPositiveOfferAmounts(Mode.Assert);
+ _establishOfferAmountMatchesAmountLocked(Mode.Assert, bytes32(0));
+ }
+
+ /**
+ * Configure the model of the OfferLocker for every offer in the list to
+ * follow the assumption that unlockOffers will not revert.
+ */
+ function _guaranteeUnlockAlwaysSucceeds() internal {
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ PendingOffer storage offer = _termAuctionList.offers[current];
+ TermAuctionOfferLocker offerLocker =
+ TermAuctionOfferLocker(address(offer.offerLocker));
+
+ offerLocker.guaranteeUnlockAlwaysSucceeds();
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume that all RepoTokens in the PendingOffers have no discount rate
+ * set in the RepoTokenList.
+ */
+ function _assumeNoDiscountRatesSet() internal {
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ address repoToken = _termAuctionList.offers[current].repoToken;
+ uint256 discountRate = _repoTokenList.discountRates[repoToken];
+ vm.assume(discountRate == RepoTokenList.INVALID_AUCTION_RATE);
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Assume that all RepoTokens in the PendingOffers pass the checks performed
+ * in validateRepoToken, to ensure the function won't revert if they need to
+ * be inserted in the RepoTokenList.
+ */
+ function _assumeRepoTokensValidate(address asset) internal {
+ bytes32 current = _termAuctionList.head;
+
+ while (current != TermAuctionList.NULL_NODE) {
+ address repoToken = _termAuctionList.offers[current].repoToken;
+ (
+ uint256 redemptionTimestamp,
+ address purchaseToken,
+ ,
+ address collateralManager
+ ) = ITermRepoToken(repoToken).config();
+
+ vm.assume(purchaseToken == asset);
+
+ uint256 numTokens = ITermRepoCollateralManager(collateralManager).numOfAcceptedCollateralTokens();
+
+ for (uint256 i; i < numTokens; i++) {
+ address currentToken = ITermRepoCollateralManager(collateralManager).collateralTokens(i);
+ uint256 minCollateralRatio = _repoTokenList.collateralTokenParams[currentToken];
+
+ vm.assume(minCollateralRatio != 0);
+ vm.assume(
+ ITermRepoCollateralManager(collateralManager).maintenanceCollateralRatios(currentToken) >= minCollateralRatio
+ );
+ }
+
+ current = _termAuctionList.nodes[current].next;
+ }
+ }
+
+ /**
+ * Test that removeCompleted preserves the list invariants.
+ */
+ function testRemoveCompleted(address asset) external {
+ // For simplicity, assume that the RepoTokenList is empty
+ _repoTokenList.head = RepoTokenList.NULL_NODE;
+
+ // Initialize a DiscountRateAdapter with symbolic storage
+ TermDiscountRateAdapter discountRateAdapter =
+ new TermDiscountRateAdapter();
+ _initializeDiscountRateAdapter(discountRateAdapter);
+
+ // Our initialization procedure guarantees these invariants,
+ // so we assert instead of assuming
+ _establishSortedByAuctionId(Mode.Assert);
+ _establishNoDuplicateOffers(Mode.Assert);
+
+ // Assume that the invariants hold before the function is called
+ _establishOfferAmountMatchesAmountLocked(Mode.Assume, bytes32(0));
+
+ // Assume that the calls to unlockOffers will not revert
+ _guaranteeUnlockAlwaysSucceeds();
+
+ // Assume that the RepoTokens in PendingOffers have no discount rate set
+ _assumeNoDiscountRatesSet();
+
+ // Assume that the RepoTokens in PendingOffers pass validation
+ _assumeRepoTokensValidate(asset);
+
+ // Save the number of tokens in the list before the function is called
+ uint256 count = _countOffersInList();
+
+ // Call the function being tested
+ _termAuctionList.removeCompleted(
+ _repoTokenList,
+ discountRateAdapter,
+ asset
+ );
+
+ // Assert that the size of the list is less than or equal to before
+ assert(_countOffersInList() <= count);
+
+ // Assert that the invariants are preserved
+ _establishSortedByAuctionId(Mode.Assert);
+ _establishNoDuplicateOffers(Mode.Assert);
+ _establishOfferAmountMatchesAmountLocked(Mode.Assert, bytes32(0));
+
+ // Now the following invariants should hold as well
+ _establishNoCompletedAuctions(Mode.Assert);
+ _establishPositiveOfferAmounts(Mode.Assert);
+ }
+}
diff --git a/src/test/kontrol/TermAuctionOfferLocker.sol b/src/test/kontrol/TermAuctionOfferLocker.sol
new file mode 100644
index 00000000..b2fd9d8c
--- /dev/null
+++ b/src/test/kontrol/TermAuctionOfferLocker.sol
@@ -0,0 +1,107 @@
+pragma solidity 0.8.23;
+
+import "src/interfaces/term/ITermAuctionOfferLocker.sol";
+
+import "src/test/kontrol/Constants.sol";
+import "src/test/kontrol/KontrolTest.sol";
+
+contract TermAuctionOfferLocker is ITermAuctionOfferLocker, KontrolTest {
+ mapping(bytes32 => TermAuctionOffer) _lockedOffers;
+ address _termRepoServicer;
+ bool _unlockAlwaysSucceeds;
+
+ uint256 private constant lockedOffersSlot = 27;
+ uint256 private constant repoServicerAndUnlockSlot = 28;
+
+ function lockedOfferSlot(bytes32 offerId) internal view returns (uint256) {
+ return uint256(keccak256(abi.encodePacked(uint256(offerId), uint256(lockedOffersSlot))));
+ }
+
+ function initializeSymbolic(address termRepoServicer) public {
+ kevm.symbolicStorage(address(this));
+ // Clear slot which holds two contract fields
+ _storeUInt256(address(this), repoServicerAndUnlockSlot, 0);
+ _termRepoServicer = termRepoServicer;
+ _unlockAlwaysSucceeds = false;
+ }
+
+ function initializeSymbolicLockedOfferFor(bytes32 offerId) public {
+ TermAuctionOffer storage offer = _lockedOffers[offerId];
+ offer.amount = freshUInt256();
+ vm.assume(offer.amount < ETH_UPPER_BOUND);
+
+ uint256 _purchaseToken = uint160(kevm.freshAddress());
+ uint256 _isRevealed = uint96(kevm.freshBool());
+ bytes memory offerLastSlotAbi = abi.encodePacked(uint96(_isRevealed), uint160(_purchaseToken));
+ bytes32 offerLastSlot;
+ assembly {
+ offerLastSlot := mload(add(offerLastSlotAbi, 0x20))
+ }
+ _storeBytes32(address(this), lockedOfferSlot(offerId) + 5, offerLastSlot);
+ }
+
+ function guaranteeUnlockAlwaysSucceeds() external {
+ _unlockAlwaysSucceeds = true;
+ }
+
+ function termRepoId() external view returns (bytes32) {
+ return bytes32(freshUInt256());
+ }
+
+ function termAuctionId() external view returns (bytes32) {
+ return bytes32(freshUInt256());
+ }
+
+ function auctionStartTime() external view returns (uint256) {
+ return freshUInt256();
+ }
+
+ function auctionEndTime() external view returns (uint256) {
+ return freshUInt256();
+ }
+
+ function revealTime() external view returns (uint256) {
+ return freshUInt256();
+ }
+
+ function purchaseToken() external view returns (address) {
+ return kevm.freshAddress();
+ }
+
+ function termRepoServicer() external view returns (address) {
+ return _termRepoServicer;
+ }
+
+ function lockedOffer(bytes32 id) external view returns (TermAuctionOffer memory) {
+ return _lockedOffers[id];
+ }
+
+ function lockedOfferAmount(bytes32 id) external view returns (uint256) {
+ return _lockedOffers[id].amount;
+ }
+
+ function lockOffers(
+ TermAuctionOfferSubmission[] calldata offerSubmissions
+ ) external returns (bytes32[] memory) {
+ kevm.symbolicStorage(address(this));
+
+ uint256 length = freshUInt256();
+ bytes32[] memory offers = new bytes32[](length);
+
+ for (uint256 i = 0; i < length; ++i) {
+ offers[i] = bytes32(freshUInt256());
+ }
+
+ return offers;
+ }
+
+ function unlockOffers(bytes32[] calldata offerIds) external {
+ // Function might revert in some cases
+ if (!_unlockAlwaysSucceeds) {
+ require(kevm.freshBool() != 0);
+ }
+
+ kevm.symbolicStorage(_termRepoServicer);
+ kevm.symbolicStorage(address(this));
+ }
+}
diff --git a/src/test/kontrol/TermDiscountRateAdapter.sol b/src/test/kontrol/TermDiscountRateAdapter.sol
new file mode 100644
index 00000000..6226df6b
--- /dev/null
+++ b/src/test/kontrol/TermDiscountRateAdapter.sol
@@ -0,0 +1,37 @@
+pragma solidity 0.8.23;
+
+import "src/interfaces/term/ITermDiscountRateAdapter.sol";
+
+import "src/test/kontrol/Constants.sol";
+import "src/test/kontrol/KontrolTest.sol";
+
+contract TermDiscountRateAdapter is ITermDiscountRateAdapter, KontrolTest {
+ mapping(address => uint256) _repoRedemptionHaircut;
+ mapping(address => uint256) _discountRate;
+
+ function initializeSymbolic() public {
+ kevm.symbolicStorage(address(this));
+ }
+
+ function initializeSymbolicParamsFor(address repoToken) public {
+ uint256 repoRedemptionHaircut = freshUInt256();
+ vm.assume(repoRedemptionHaircut <= 1e18);
+ _repoRedemptionHaircut[repoToken] = repoRedemptionHaircut;
+
+ uint256 discountRate = freshUInt256();
+ vm.assume(discountRate < ETH_UPPER_BOUND);
+ _discountRate[repoToken] = discountRate;
+ }
+
+ function repoRedemptionHaircut(address repoToken) external view returns (uint256) {
+ return _repoRedemptionHaircut[repoToken];
+ }
+
+ function getDiscountRate(address repoToken) external view returns (uint256) {
+ return _discountRate[repoToken];
+ }
+
+ function TERM_CONTROLLER() external view returns (ITermController) {
+ return ITermController(kevm.freshAddress());
+ }
+}
diff --git a/src/test/kontrol/TermRepoCollateralManager.sol b/src/test/kontrol/TermRepoCollateralManager.sol
new file mode 100644
index 00000000..4b1d518d
--- /dev/null
+++ b/src/test/kontrol/TermRepoCollateralManager.sol
@@ -0,0 +1,47 @@
+pragma solidity 0.8.23;
+
+import "src/interfaces/term/ITermRepoCollateralManager.sol";
+import "src/test/kontrol/KontrolTest.sol";
+
+contract TermRepoCollateralManager is ITermRepoCollateralManager, KontrolTest {
+ mapping(address => uint256) _maintenanceCollateralRatios;
+ address[] _collateralTokens;
+
+ uint256 private constant collateralTokensSlot = 28;
+
+ function collateralTokensDataSlot(uint256 i) internal view returns (uint256) {
+ return uint256(keccak256(abi.encodePacked(collateralTokensSlot))) + i;
+ }
+
+ function initializeSymbolic() public {
+ kevm.symbolicStorage(address(this));
+
+ // For simplicity, choose an arbitrary number of collateral tokens: 3
+ _storeUInt256(address(this), collateralTokensSlot, 2);
+
+ for (uint256 i = 0; i < _collateralTokens.length; ++i) {
+ // Generate an arbitrary concrete address for each token
+ // All repoTokens in the list will share the same colllateral tokens
+ address currentToken = address(
+ uint160(uint256(keccak256(abi.encodePacked("collateral", i))))
+ );
+
+ _storeUInt256(address(this), collateralTokensDataSlot(i), uint256(uint160(currentToken)));
+ _maintenanceCollateralRatios[currentToken] = freshUInt256();
+ }
+ }
+
+ function maintenanceCollateralRatios(
+ address collateralToken
+ ) external view returns (uint256) {
+ return _maintenanceCollateralRatios[collateralToken];
+ }
+
+ function numOfAcceptedCollateralTokens() external view returns (uint8) {
+ return uint8(_collateralTokens.length);
+ }
+
+ function collateralTokens(uint256 index) external view returns (address) {
+ return _collateralTokens[index];
+ }
+}
diff --git a/src/test/kontrol/TermRepoServicer.sol b/src/test/kontrol/TermRepoServicer.sol
new file mode 100644
index 00000000..964cef40
--- /dev/null
+++ b/src/test/kontrol/TermRepoServicer.sol
@@ -0,0 +1,48 @@
+pragma solidity ^0.8.23;
+
+import "src/interfaces/term/ITermRepoServicer.sol";
+import "src/test/kontrol/KontrolTest.sol";
+
+contract TermRepoServicer is ITermRepoServicer, KontrolTest {
+ address _termRepoToken;
+ bool _redeemAlwaysSucceeds;
+
+ uint256 private constant repoTokenAndRedeemSlot = 27;
+
+ function initializeSymbolic(address termRepoToken) public {
+ kevm.symbolicStorage(address(this));
+ // Clear slot which holds two contract fields
+ _storeUInt256(address(this), repoTokenAndRedeemSlot, 0);
+ _termRepoToken = termRepoToken;
+ _redeemAlwaysSucceeds = false;
+ }
+
+ function guaranteeRedeemAlwaysSucceeds() external {
+ _redeemAlwaysSucceeds = true;
+ }
+
+ function redeemTermRepoTokens(
+ address redeemer,
+ uint256 amountToRedeem
+ ) external {
+ // Function might revert in some cases
+ if (!_redeemAlwaysSucceeds) {
+ require(kevm.freshBool() != 0);
+ }
+
+ kevm.symbolicStorage(_termRepoToken);
+ kevm.symbolicStorage(address(this));
+ }
+
+ function termRepoToken() external view returns (address) {
+ return _termRepoToken;
+ }
+
+ function termRepoLocker() external view returns (address) {
+ return kevm.freshAddress();
+ }
+
+ function purchaseToken() external view returns (address) {
+ return kevm.freshAddress();
+ }
+}
diff --git a/src/test/kontrol/term-lemmas.k b/src/test/kontrol/term-lemmas.k
new file mode 100644
index 00000000..6dabe3bd
--- /dev/null
+++ b/src/test/kontrol/term-lemmas.k
@@ -0,0 +1,257 @@
+requires "evm.md"
+requires "foundry.md"
+// requires "keccak.md"
+
+module TERM-LEMMAS
+ imports EVM
+ imports FOUNDRY
+ imports INT-SYMBOLIC
+ imports MAP-SYMBOLIC
+ imports SET-SYMBOLIC
+ // imports KECCAK-LEMMAS
+
+ syntax StepSort ::= Int
+ | Bool
+ | Bytes
+ | Map
+ | Set
+ // -------------------------
+
+ syntax KItem ::= runLemma ( StepSort )
+ | doneLemma( StepSort )
+ // --------------------------------------
+ rule runLemma(T) => doneLemma(T) ...
+
+ // Instantiation of the Kontrol lemma
+ // rule ( X ==Int ( X *Int Y ) /Word Y ) orBool Y ==Int 0 => true [simplification, preserves-definedness]
+ // when Y is of the form Y *Int Z, because then the `Y ==Int 0` condition gets simplified to a different form
+ rule ( X ==Int ( X *Int (Y *Int Z) ) /Word (Y *Int Z) ) orBool (Y ==Int 0 orBool Z ==Int 0) => true [simplification, preserves-definedness]
+
+ // Instantiation of the Kontrol lemma
+ // rule ( X ==Int ( X *Int Y ) /Word Y ) orBool Y ==Int 0 => true [simplification, preserves-definedness]
+ // when Y is of the form Y *Int ( Z *Int T ), because then the `Y ==Int 0` condition gets simplified to a different form
+ rule ( X ==Int ( X *Int (Y *Int (Z *Int T)) ) /Word (Y *Int (Z *Int T)) ) orBool (Y ==Int 0 orBool (Z ==Int 0 orBool T ==Int 0)) => true [simplification, preserves-definedness]
+
+ // Instantiation of the Kontrol lemma
+ // rule ( X ==Int ( X *Int Y ) /Word Y ) orBool Y ==Int 0 => true [simplification, preserves-definedness]
+ // when Y is of the form Y /Int Z, because then the `Y ==Int 0` condition gets simplified to a different form
+ rule ( X ==Int ( X *Int (Y /Int Z) ) /Word (Y /Int Z) ) orBool Y true
+ requires notBool ( Z ==Int 0 )
+ [simplification, preserves-definedness]
+
+ rule [chop-sub]:
+ chop ( Y -Int X:Int ) ==Int 0 => X ==Int Y
+ requires #rangeUInt(256, X) andBool #rangeUInt(256, Y)
+ [simplification, concrete(Y), comm]
+
+ rule [keccak-slots-disjoint]:
+ keccak ( A ) ==Int keccak ( B ) +Int C => false
+ requires notBool 0 ==Int C
+ [simplification, concrete(C), comm]
+
+ rule [keccak-slots-disjoint-ml-l]:
+ { keccak ( A ) #Equals keccak ( B ) +Int C } => #Bottom
+ requires notBool 0 ==Int C
+ [simplification, concrete(C)]
+
+ rule [keccak-slots-disjoint-ml-r]:
+ { keccak ( B ) +Int C #Equals keccak ( A ) } => #Bottom
+ requires notBool 0 ==Int C
+ [simplification, concrete(C)]
+
+ // 2a. |Int and +Bytes, update to be done in left
+ rule [bor-update-to-left-2]:
+ #asWord ( B1 +Bytes B2 ) |Int A =>
+ #asWord ( #buf ( 32 -Int lengthBytes(B2), (A /Int (2 ^Int (8 *Int lengthBytes(B2)))) |Int #asWord ( B1 ) ) +Bytes B2 )
+ requires #rangeUInt(256, A) andBool A modInt (2 ^Int (8 *Int lengthBytes(B2))) ==Int 0 andBool lengthBytes(B1 +Bytes B2) <=Int 32
+ [simplification, preserves-definedness]
+
+ // 2b. |Int of +Bytes, update to be done in right
+ rule [bor-update-to-right-2]:
+ #asWord ( B1 +Bytes B2 ) |Int A =>
+ #asWord ( B1 +Bytes #buf ( lengthBytes(B2), A |Int #asWord ( B2 ) ) )
+ requires 0 <=Int A andBool A SetItem(X) ( S |Set SetItem (Y) ) requires notBool X ==Int Y [simplification, preserves-definedness]
+ rule ( SetItem(X:Int) S:Set ) |Set SetItem(Y:Int) => SetItem(X) S requires X ==Int Y [simplification, preserves-definedness]
+ rule .Set |Set SetItem(X:Int) => SetItem(X) [simplification, preserves-definedness]
+
+ rule K1 in_keys((K2 |-> _ ) M) => K1 ==Int K2 orBool K1 in_keys(M) [simplification]
+
+ rule [transferFunds-hp-neq]:
+ #transferFunds ACCTFROM ACCTTO VALUE => .K ...
+
+ ACCTFROM
+ ORIGFROM => ORIGFROM -Word VALUE
+ ...
+
+
+ ACCTTO
+ ORIGTO => ORIGTO +Word VALUE
+ ...
+
+ requires ACCTFROM =/=K ACCTTO andBool VALUE <=Int ORIGFROM
+ [priority(30), preserves-definedness]
+
+ rule [accounts-in-keys]:
+ `AccountCellMap:in_keys`(
+ (X:Int) ,
+ (`_AccountCellMap_`(AccountCellMapItem( Y:Int , _:AccountCell):AccountCellMap, ACCOUNTS_REST:AccountCellMap)):AccountCellMap
+ ) => X ==Int Y orBool `AccountCellMap:in_keys`( (X:Int) , ACCOUNTS_REST)
+ [simplification, preserves-definedness]
+
+endmodule
+
+module TERM-LEMMAS-SPEC
+ imports TERM-LEMMAS
+
+ claim [storage-offset]: runLemma ( ( #lookup ( STORAGE3:Map , 2 ) /Int pow160 ) ) => doneLemma ( #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE3:Map , 2 ) ) , 0 , 12 ) ) ) ...
+
+ claim [chop-simplify]: runLemma (
+ notBool chop ( WORD7:Int +Int ( WORD12:Int *Int ( ( WORD5:Int -Int WORD6:Int ) /Int WORD11:Int ) ) ) ==Int
+ chop ( chop ( WORD7:Int +Int ( WORD12:Int *Int ( ( WORD5:Int -Int WORD6:Int ) /Int WORD11:Int ) ) ) *Int 1000000000000000000 ) /Int 1000000000000000000
+ ) => runLemma ( false ) ...
+ requires 0 <=Int WORD5:Int
+ andBool 0 <=Int WORD6:Int
+ andBool 0 <=Int WORD7:Int
+ andBool 0 <=Int WORD11:Int
+ andBool 0 <=Int WORD12:Int
+ andBool WORD11:Int =/=Int 0
+ andBool WORD12:Int =/=Int 0
+ andBool WORD6:Int <=Int WORD5:Int
+ andBool WORD5:Int runLemma (
+ STORAGE0:Map
+ [ 5 <- #asWord ( b"\x00E\xc9,,\xd0\xdf{-p^\xf1,\xffw\xcb\x0b\xc5W\xed\"" +Bytes #buf ( 5 , WORD4:Int ) +Bytes #buf ( 5 , WORD3:Int ) +Bytes b"\x00" ) ]
+ [ 6 <- #asWord ( #buf ( 1 , WORD7:Int ) +Bytes b"\xa4\xadOh\xd0\xb9\x1c\xfd\x19h|\x88\x1eP\xf3\xa0\x02B\x82\x8c" +Bytes #buf ( 5 , WORD6:Int ) +Bytes #buf ( 5 , WORD5:Int ) ) ]
+ [ 5 <- #asWord ( b"\x00E\xc9,,\xd0\xdf{-p^\xf1,\xffw\xcb\x0b\xc5W\xed\"" +Bytes #buf ( 5 , WORD4:Int ) +Bytes #buf ( 5 , TIMESTAMP_CELL:Int ) +Bytes b"\x01" ) ]
+ [ 6 <- ( ( TIMESTAMP_CELL:Int *Int pow40 ) |Int ( 115792089237316195423570985008687907853269984665640562830531764394383466561535 &Int #asWord ( #buf ( 1 , WORD7:Int ) +Bytes b"\xa4\xadOh\xd0\xb9\x1c\xfd\x19h|\x88\x1eP\xf3\xa0\x02B\x82\x8c" +Bytes #buf ( 5 , WORD6:Int ) +Bytes #buf ( 5 , WORD5:Int ) ) ) ) ]
+ [ 5 <- #asWord ( b"\x00E\xc9,,\xd0\xdf{-p^\xf1,\xffw\xcb\x0b\xc5W\xed\"" +Bytes #buf ( 5 , TIMESTAMP_CELL:Int ) +Bytes #buf ( 5 , TIMESTAMP_CELL:Int ) +Bytes b"\x01" ) ]
+ ) => doneLemma (
+ STORAGE0:Map
+ [ 6 <- #asWord ( #buf ( 1 , WORD7:Int ) +Bytes b"\xa4\xadOh\xd0\xb9\x1c\xfd\x19h|\x88\x1eP\xf3\xa0\x02B\x82\x8c" +Bytes #buf ( 5 , TIMESTAMP_CELL:Int ) +Bytes #buf ( 5 , WORD5:Int ) ) ]
+ [ 5 <- #asWord ( b"\x00E\xc9,,\xd0\xdf{-p^\xf1,\xffw\xcb\x0b\xc5W\xed\"" +Bytes #buf ( 5 , TIMESTAMP_CELL:Int ) +Bytes #buf ( 5 , TIMESTAMP_CELL:Int ) +Bytes b"\x01" ) ]
+ ) ...
+ requires 0 <=Int TIMESTAMP_CELL andBool TIMESTAMP_CELL runLemma(
+ ( maxUInt8 &Int ( ( TIMESTAMP_CELL:Int *Int pow48 ) |Int ( 115792089237316195423570985008687907853269984665640254554447762944319381569535 &Int ( ( TIMESTAMP_CELL:Int *Int 256 ) |Int ( 115792089237316195423570985008687907853269984665640564039457583726438152929535 &Int ( 1 |Int #asWord ( b"\x00E\xc9,,\xd0\xdf{-p^\xf1,\xffw\xcb\x0b\xc5W\xed\"" +Bytes #buf ( 5 , _WORD4:Int ) +Bytes #buf ( 5 , _WORD3:Int ) +Bytes b"\x00" ) ) ) ) ) ) )
+ ) => doneLemma(
+ 1
+ ) ...
+ requires 0 <=Int TIMESTAMP_CELL andBool TIMESTAMP_CELL runLemma (
+ ( ( maxUInt40 &Int ( ( 115341543235797707419527244145998463631733976271937281205136574426583511597055 &Int #asWord ( #buf ( 2 , WORD7:Int ) +Bytes b"\xa4\xadOh\xd0\xb9\x1c\xfd\x19h|\x88\x1eP\xf3\xa0\x02B\x82\x8c" +Bytes #buf ( 5 , TIMESTAMP_CELL:Int ) +Bytes #buf ( 5 , WORD5:Int ) ) ) /Int pow40 ) ) )
+ ) => doneLemma (
+ TIMESTAMP_CELL
+ ) ...
+ requires 0 <=Int TIMESTAMP_CELL andBool TIMESTAMP_CELL runLemma (
+ ( maxUInt40 &Int ( ( TIMESTAMP_CELL:Int |Int ( 115792089237316195423570985008687907853269984665640564039457584006813618012160 &Int #asWord ( #buf ( 1 , WORD7:Int ) +Bytes b"\xa4\xadOh\xd0\xb9\x1c\xfd\x19h|\x88\x1eP\xf3\xa0\x02B\x82\x8c" +Bytes #buf ( 5 , WORD6:Int ) +Bytes #buf ( 5 , WORD5:Int ) ) ) ) /Int pow40 ) ) <=Int TIMESTAMP_CELL:Int
+ ) => doneLemma (
+ true
+ ) ...
+ requires 0 <=Int TIMESTAMP_CELL andBool TIMESTAMP_CELL runLemma (
+ #asWord ( #range ( #buf ( 10 , ( ( ( TIMESTAMP_CELL:Int *Int pow48 ) /Int 256 ) |Int TIMESTAMP_CELL:Int ) ) , 5 , 5 ) ) <=Int TIMESTAMP_CELL:Int
+ ) => doneLemma (
+ true
+ ) ...
+ requires 0 <=Int TIMESTAMP_CELL andBool TIMESTAMP_CELL runLemma (
+ #asWord ( #range ( #buf ( 6 , TIMESTAMP_CELL:Int *Int 256 ) , 5 , 1 ) )
+ ) => doneLemma (
+ false
+ ) ...
+ requires 0 <=Int TIMESTAMP_CELL andBool TIMESTAMP_CELL runLemma (
+ #asWord ( #range ( #buf ( 26 , 960911443338137442927181681227604902095826437272264907948032 |Int WORD4:Int ) , 21 , 5 ) ) <=Int TIMESTAMP_CELL:Int
+ ) => doneLemma (
+ WORD4 <=Int TIMESTAMP_CELL
+ ) ...
+ requires 0 <=Int WORD4 andBool WORD4 runLemma (
+ #asWord ( #range ( #buf ( 25 , ( ( ( WORD7:Int +Int 1 ) *Int pow200 ) |Int #asWord ( #buf ( 25 , ( 438052756531465687819472504520361015472122898704787692322816 |Int WORD6:Int ) ) ) ) ) , 20 , 5 ) ) <=Int TIMESTAMP_CELL:Int
+ ) => doneLemma (
+ WORD6 <=Int TIMESTAMP_CELL
+ ) ...
+ requires 0 <=Int WORD6 andBool WORD6 runLemma (
+ #asWord ( #buf ( 20 , 770621190285571058874329108704665103402425909248 |Int ( ( WORD7:Int +Int 1 ) *Int pow160 ) ) )
+ ) => doneLemma (
+ 770621190285571058874329108704665103402425909248
+ ) ...
+ requires 0 <=Int WORD7 andBool WORD7 runLemma (
+ ( 481644099385675654177479669474857658256926169505224677670350078624137216 |Int ( 115790322390251417039241401711187164934754157181743689629425282016341011726335 &Int #asWord ( #buf ( 2 , WORD7:Int ) +Bytes b"\xa4\xadOh\xd0\xb9\x1c\xfd\x19h|\x88\x1eP\xf3\xa0\x02B\x82\x8c" +Bytes #buf ( 5 , WORD6:Int ) +Bytes #buf ( 5 , WORD5:Int ) ) ) )
+ ) => doneLemma (
+ #asWord ( #buf ( 2 , WORD7:Int ) +Bytes b"E\xc9,,\xd0\xdf{-p^\xf1,\xffw\xcb\x0b\xc5W\xed\"" +Bytes #buf ( 5 , WORD6:Int ) +Bytes #buf ( 5 , WORD5:Int ) )
+ ) ...
+ requires 0 <=Int WORD5 andBool WORD5 runLemma ( lengthBytes ( #padToWidth ( 32 , #asByteStack ( ( ( ( ( ( WORD0:Int *Int VV0_amount_114b9705:Int ) /Int WORD:Int ) +Int WORD8:Int ) |Int #asWord ( #buf ( 16 , WORD9:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ) ) /Int pow128 ) ) ) ) ) => doneLemma ( 32 ) ...
+
+ claim [add-not-eq-zero]: runLemma ( 0 ==Int ( ( ( ( ( VV0_purchaseTokenPrecision_114b9705:Int *Int ( WORD0:Int -Int TIMESTAMP_CELL:Int ) ) /Int 31104000 ) *Int WORD:Int ) /Int 1000000000000000000 ) +Int VV0_purchaseTokenPrecision_114b9705:Int ) ) => doneLemma ( false ) ...
+ requires 0 runLemma ( 0 ==Int chop ( ( 491460923342184218035706888008750043977755113263 -Int VV0_repoToken_114b9705 ) ) ) => doneLemma ( 491460923342184218035706888008750043977755113263 ==Int VV0_repoToken_114b9705 ) ...
+ requires 0 <=Int VV0_repoToken_114b9705:Int
+ andBool VV0_repoToken_114b9705:Int runLemma ( #lookup ( STORAGE:Map [ maxUInt5 <- WORD:Int ] [ keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) <- 491460923342184218035706888008750043977755113263 ] [ ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 1 ) <- WORD0:Int ] [ ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 2 ) <- 263400868551549723330807389252719309078400616203 ] [ ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 3 ) <- 1405310203571408291950365054053061012934685786634 ] , keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " ) ) ) => doneLemma ( #lookup ( STORAGE:Map , keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " ) ) ) ...
+
+ claim [keccak-lookup-2]: runLemma ( #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE:Map [ maxUInt5 <- WORD:Int ] [ keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) <- 491460923342184218035706888008750043977755113263 ] [ ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 1 ) <- WORD0:Int ] [ ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 2 ) <- 263400868551549723330807389252719309078400616203 ] [ ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 3 ) <- 1405310203571408291950365054053061012934685786634 ] [ keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " ) <- WORD1:Int ] [ keccak ( #buf ( 32 , WORD1:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) <- 511347974759188522659820409854212399244223280809 ] [ ( keccak ( #buf ( 32 , WORD1:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 1 ) <- WORD2:Int ] [ ( keccak ( #buf ( 32 , WORD1:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 2 ) <- 1136628940260574992893479910319181283093952727985 ] , ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 3 ) ) ) , 12 , 20 ) ) ) => doneLemma ( 1405310203571408291950365054053061012934685786634 ) ...
+
+ claim [keccak-comparison-1]: runLemma ( ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 2 ) ==Int ( keccak ( #buf ( 32 , WORD1:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 3 ) ) => doneLemma ( false ) ...
+
+ claim [keccak-comparison-2]: runLemma ( ( keccak ( #buf ( 32 , WORD:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 3 ) ==Int ( keccak ( #buf ( 32 , WORD1:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 2 ) ) => doneLemma ( false ) ...
+
+ claim [chop-check]: runLemma ( chop ( ( VV2_purchaseTokenPrecision_114b9705:Int *Int WORD8:Int *Int WORD10:Int /Int 1000000000000000000000000000000000000 ) *Int ( WORD11:Int -Int TIMESTAMP_CELL:Int ) +Int ( VV2_purchaseTokenPrecision_114b9705:Int *Int WORD1:Int *Int WORD3:Int /Int 1000000000000000000000000000000000000 ) *Int ( WORD4:Int -Int TIMESTAMP_CELL:Int ) ) ) => doneLemma ( true ) ...
+ requires 0 <=Int VV2_purchaseTokenPrecision_114b9705 andBool VV2_purchaseTokenPrecision_114b9705 runLemma ( #lookup ( STORAGE:Map [ 34 <- #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE:Map , 34 ) ) , 0 , 12 ) +Bytes b"V\x15\xde\xb7\x98\xbb>M\xfa\x019\xdf\xa1\xb3\xd43\xcc#\xb7/" ) ] [ maxUInt5 <- VV0_offerId_114b9705:Int ] [ keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " ) <- 0 ] [ keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) <- #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE:Map , keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) ) ) , 0 , 12 ) +Bytes b".#M\xaeu\xc7\x93\xf6z5\x08\x9c\x9d\x99$^\x1cXG\x0b" ) ] [ ( keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 1 ) <- #lookup ( STORAGE3:Map , ( keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b" ) +Int 4 ) ) ] [ ( keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 2 ) <- #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE:Map [ 34 <- #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE:Map , 34 ) ) , 0 , 12 ) +Bytes b"V\x15\xde\xb7\x98\xbb>M\xfa\x019\xdf\xa1\xb3\xd43\xcc#\xb7/" ) ] [ maxUInt5 <- VV0_offerId_114b9705:Int ] [ keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " ) <- 0 ] , ( keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 2 ) ) ) , 0 , 12 ) +Bytes #buf ( 20 , WORD:Int ) ) ] [ ( keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 3 ) <- #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE:Map [ 34 <- #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE:Map , 34 ) ) , 0 , 12 ) +Bytes b"V\x15\xde\xb7\x98\xbb>M\xfa\x019\xdf\xa1\xb3\xd43\xcc#\xb7/" ) ] [ maxUInt5 <- VV0_offerId_114b9705:Int ] [ keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " ) <- 0 ] , ( keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 3 ) ) ) , 0 , 12 ) +Bytes b"\xf6(I\xf9\xa0\xb5\xbf)\x13\xb3\x96\t\x8f|p\x19\xb5\x1a\x82\n" ) ] , keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " ) ) )
+ => doneLemma ( 0 ) ...
+
+ claim [slot-update-10]: runLemma ( #asWord ( #range ( #buf ( 32 , ( #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE:Map , ( keccak ( #buf ( 32 , VV0_offerId_114b9705:Int ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!" ) +Int 2 ) ) ) , 0 , 12 ) +Bytes b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ) |Int WORD:Int ) ) , 12 , 20 ) ) )
+ => doneLemma ( WORD:Int ) ...
+ requires #rangeUInt(160, WORD)
+
+endmodule
\ No newline at end of file
diff --git a/src/test/mocks/MockTermAuctionOfferLocker.sol b/src/test/mocks/MockTermAuctionOfferLocker.sol
index 7748ea37..cba9968d 100644
--- a/src/test/mocks/MockTermAuctionOfferLocker.sol
+++ b/src/test/mocks/MockTermAuctionOfferLocker.sol
@@ -14,11 +14,11 @@ contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker {
MockTermRepoLocker internal repoLocker;
ITermAuction internal auction;
mapping(bytes32 => TermAuctionOffer) internal lockedOffers;
-
+
constructor(
- ITermAuction _auction,
- address _repoLocker,
- address _repoServicer,
+ ITermAuction _auction,
+ address _repoLocker,
+ address _repoServicer,
address _purchaseToken
) {
auction = _auction;
@@ -48,6 +48,10 @@ contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker {
return lockedOffers[id];
}
+ function lockedOfferAmount(bytes32 id) external view returns (uint256) {
+ return lockedOffers[id].amount;
+ }
+
function lockOffers(
TermAuctionOfferSubmission[] calldata offerSubmissions
) external returns (bytes32[] memory offerIds) {
@@ -77,7 +81,7 @@ contract MockTermAuctionOfferLocker is ITermAuctionOfferLocker {
offer.purchaseToken = submission.purchaseToken;
repoLocker.lockPurchaseTokens(msg.sender, offer.amount);
- }
+ }
lockedOffers[offer.id] = offer;
offerIds[i] = offer.id;
}