diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2cfc1d2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.spec linguist-language=Solidity +*.conf linguist-detectable +*.conf linguist-language=JSON5 diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml new file mode 100644 index 0000000..a0055e8 --- /dev/null +++ b/.github/workflows/certora.yml @@ -0,0 +1,46 @@ +name: Certora + +on: [push, pull_request] + +jobs: + certora: + name: Certora + runs-on: ubuntu-latest + strategy: + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11' + java-package: jre + + - name: Set up Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: 3.13 + + - name: Install solc-select + run: pip3 install solc-select + + - name: Solc Select 0.8.24 + run: solc-select install 0.8.24 + + - name: Solc Select 0.8.21 + run: solc-select install 0.8.21 + + - name: Solc Select 0.5.12 + run: solc-select install 0.5.12 + + - name: Install Certora + run: pip3 install certora-cli-beta + + - name: Certora verify SPBEAM + run: make certora-spbeam + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} + diff --git a/.gitignore b/.gitignore index 05db85e..65d4abc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ docs/ # Ignores script config /script/input/**/*.json !/script/input/**/template-*.json -/script/output/**/*.json \ No newline at end of file +/script/output/**/*.json + +# Certora +.certora_internal/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c6603f6 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +PATH := ~/.solc-select/artifacts/solc-0.8.24:~/.solc-select/artifacts/solc-0.8.21:~/.solc-select/artifacts/solc-0.5.12:~/.solc-select/artifacts:$(PATH) +certora-spbeam:; PATH=${PATH} certoraRun certora/SPBEAM.conf$(if $(rule), --rule $(rule),) + diff --git a/certora/SPBEAM.conf b/certora/SPBEAM.conf new file mode 100644 index 0000000..2286322 --- /dev/null +++ b/certora/SPBEAM.conf @@ -0,0 +1,57 @@ +{ + "files": [ + "src/SPBEAM.sol", + "certora/mocks/Conv.sol", + "certora/mocks/Jug.sol", + "certora/mocks/Pot.sol", + "certora/mocks/SUsds.sol", + "certora/mocks/Usds.sol", + "certora/mocks/UsdsJoin.sol", + "certora/mocks/Vat.sol" + ], + "link": [ + "Jug:vat=Vat", + "Pot:vat=Vat", + "SUsds:vat=Vat", + "UsdsJoin:usds=Usds", + "UsdsJoin:vat=Vat", + "SPBEAM:jug=Jug", + "SPBEAM:pot=Pot", + "SPBEAM:susds=SUsds", + "SPBEAM:conv=Conv" + ], + "solc_map": { + "Conv": "solc-0.8.24", + "SPBEAM": "solc-0.8.24", + "Jug": "solc-0.5.12", + "Pot": "solc-0.5.12", + "SUsds": "solc-0.8.21", + "Usds": "solc-0.8.21", + "UsdsJoin": "solc-0.8.21", + "Vat": "solc-0.5.12" + }, + "solc_optimize_map": { + "Conv": "200", + "SPBEAM": "200", + "Jug": "0", + "Pot": "0", + "SUsds": "200", + "Usds": "200", + "UsdsJoin": "200", + "Vat": "0" + }, + "verify": "SPBEAM:certora/SPBEAM.spec", + "parametric_contracts": [ + "SPBEAM" + ], + "build_cache": true, + "loop_iter": "3", + "multi_assert_check": true, + "optimistic_loop": true, + "process": "emv", + "prover_args": [ + " -rewriteMSizeAllocations true" + ], + "rule_sanity": "basic", + "wait_for_results": "all" +} diff --git a/certora/SPBEAM.spec b/certora/SPBEAM.spec new file mode 100644 index 0000000..c851d6d --- /dev/null +++ b/certora/SPBEAM.spec @@ -0,0 +1,651 @@ +// SPBEAM.spec + +using SPBEAM as spbeam; +using Conv as conv; +using Jug as jug; +using Pot as pot; +using SUsds as susds; +using Vat as vat; + +methods { + function RAY() external returns (uint256) envfree; + function bad() external returns (uint8) envfree; + function buds(address) external returns (uint256) envfree; + function cfgs(bytes32) external returns (uint16, uint16, uint16) envfree; + function tau() external returns (uint64) envfree; + function toc() external returns (uint128) envfree; + function wards(address) external returns (uint256) envfree; + + function conv.rtob(uint256) external returns (uint256) envfree; + function conv.btor(uint256) external returns (uint256) envfree; + function conv.MAX_BPS_IN() external returns (uint256) envfree; + + function jug.ilks(bytes32) external returns (uint256, uint256) envfree; + function jug.wards(address) external returns (uint256) envfree; + + function pot.dsr() external returns (uint256) envfree; + function pot.rho() external returns (uint256) envfree; + function pot.wards(address) external returns (uint256) envfree; + + function susds.ssr() external returns (uint256) envfree; + function susds.rho() external returns (uint256) envfree; + function susds.wards(address) external returns (uint256) envfree; + + function vat.Line() external returns (uint256) envfree; + function vat.can(address, address) external returns (uint256) envfree; + function vat.dai(address) external returns (uint256) envfree; + function vat.debt() external returns (uint256) envfree; + function vat.ilks(bytes32) external returns (uint256, uint256, uint256, uint256, uint256) envfree; + function vat.live() external returns (uint256) envfree; + function vat.urns(bytes32, address) external returns (uint256, uint256) envfree; +} + +definition EMPTY_BYTES32() returns bytes32 = to_bytes32(0x0000000000000000000000000000000000000000000000000000000000000000); +definition TAU() returns bytes32 = to_bytes32(0x7461750000000000000000000000000000000000000000000000000000000000); +definition TOC() returns bytes32 = to_bytes32(0x746f630000000000000000000000000000000000000000000000000000000000); +definition BAD() returns bytes32 = to_bytes32(0x6261640000000000000000000000000000000000000000000000000000000000); +definition MIN() returns bytes32 = to_bytes32(0x6d696e0000000000000000000000000000000000000000000000000000000000); +definition MAX() returns bytes32 = to_bytes32(0x6d61780000000000000000000000000000000000000000000000000000000000); +definition STEP() returns bytes32 = to_bytes32(0x7374657000000000000000000000000000000000000000000000000000000000); +definition SSR() returns bytes32 = to_bytes32(0x5353520000000000000000000000000000000000000000000000000000000000); +definition DSR() returns bytes32 = to_bytes32(0x4453520000000000000000000000000000000000000000000000000000000000); + +// Verify that each storage variable is only modified in the expected functions +rule storage_affected(method f) { + env e; + address anyAddr; + bytes32 anyId; + + mathint wardsBefore = wards(anyAddr); + mathint budsBefore = buds(anyAddr); + mathint minBefore; mathint maxBefore; mathint stepBefore; + minBefore, maxBefore, stepBefore = cfgs(anyId); + mathint badBefore = bad(); + mathint tauBefore = tau(); + mathint tocBefore = toc(); + + calldataarg args; + f(e, args); + + mathint wardsAfter = wards(anyAddr); + mathint budsAfter = buds(anyAddr); + mathint minAfter; mathint maxAfter; mathint stepAfter; + minAfter, maxAfter, stepAfter = cfgs(anyId); + mathint badAfter = bad(); + mathint tauAfter = tau(); + mathint tocAfter = toc(); + + + assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector, "wards[x] changed in an unexpected function"; + assert budsAfter != budsBefore => f.selector == sig:kiss(address).selector || f.selector == sig:diss(address).selector, "buds[x] changed in an unexpected function"; + assert minAfter != minBefore => f.selector == sig:file(bytes32, bytes32, uint256).selector, "min[x] changed in an unexpected function"; + assert maxAfter != maxBefore => f.selector == sig:file(bytes32, bytes32, uint256).selector, "max[x] changed in an unexpected function"; + assert stepAfter != stepBefore => f.selector == sig:file(bytes32, bytes32, uint256).selector, "step[x] changed in an unexpected function"; + assert badAfter != badBefore => f.selector == sig:file(bytes32, uint256).selector, "bad changed in an unexpected function"; + assert tauAfter != tauBefore => f.selector == sig:file(bytes32, uint256).selector, "tau changed in an unexpected function"; + assert tocAfter != tocBefore => f.selector == sig:file(bytes32, uint256).selector || f.selector == sig:set(SPBEAM.ParamChange[] calldata).selector, "toc changed in an unexpected function"; +} + +// Verify that the correct storage changes for non-reverting rely +rule rely(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + rely(e, usr); + + mathint wardsOtherAfter = wards(other); + mathint wardsUsrAfter = wards(usr); + + assert wardsUsrAfter == 1, "rely did not set wards[usr]"; + assert wardsOtherAfter == wardsOtherBefore, "rely unexpectedly changed other wards[x]"; +} + +// Verify revert rules on rely +rule rely_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + rely@withrevert(e, usr); + + assert lastReverted <=> revert1 || revert2, "rely revert rules failed"; +} + +// Verify that the correct storage changes for non-reverting deny +rule deny(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + deny(e, usr); + + mathint wardsOtherAfter = wards(other); + mathint wardsUsrAfter = wards(usr); + + assert wardsUsrAfter == 0, "deny did not set wards[usr]"; + assert wardsOtherAfter == wardsOtherBefore, "deny unexpectedly changed other wards[x]"; +} + +// Verify revert rules on deny +rule deny_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + deny@withrevert(e, usr); + + assert lastReverted <=> revert1 || revert2, "deny revert rules failed"; +} + +// Verify that the correct storage changes for non-reverting kiss +rule kiss(address usr) { + env e; + + address other; + require other != usr; + + mathint budsOtherBefore = buds(other); + + kiss(e, usr); + + mathint budsOtherAfter = buds(other); + mathint budsUsrAfter = buds(usr); + + assert budsUsrAfter == 1, "kiss did not set buds[usr]"; + assert budsOtherAfter == budsOtherBefore, "kiss unexpectedly changed other buds[x]"; +} + +// Verify revert rules on kiss +rule kiss_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + kiss@withrevert(e, usr); + + assert lastReverted <=> revert1 || revert2, "kiss revert rules failed"; +} + +// Verify that the correct storage changes for non-reverting diss +rule diss(address usr) { + env e; + + address other; + require other != usr; + + mathint budsOtherBefore = buds(other); + + diss(e, usr); + + mathint budsOtherAfter = buds(other); + mathint budsUsrAfter = buds(usr); + + assert budsUsrAfter == 0, "diss did not set buds[usr]"; + assert budsOtherAfter == budsOtherBefore, "diss unexpectedly changed other buds[x]"; +} + +// Verify revert rules on diss +rule diss_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + diss@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "diss revert rules failed"; +} + +// Verify correct storage changes for non-reverting file for global parameters +rule file_global(bytes32 what, uint256 data) { + env e; + + mathint badBefore = bad(); + mathint tauBefore = tau(); + mathint tocBefore = toc(); + + file(e, what, data); + + mathint badAfter = bad(); + mathint tauAfter = tau(); + mathint tocAfter = toc(); + + assert what == BAD() => badAfter == to_mathint(data), "file did not set bad"; + assert what != BAD() => badAfter == badBefore, "file did keep unchanged bad"; + assert what == TAU() => tauAfter == to_mathint(data), "file did not set tau"; + assert what != TAU() => tauAfter == tauBefore, "file did keep unchanged tau"; + assert what == TOC() => tocAfter == to_mathint(data), "file did not set toc"; + assert what != TOC() => tocAfter == tocBefore, "file did keep unchanged toc"; +} + +// Verify revert rules on file for global parameters +rule file_global_revert(bytes32 what, uint256 data) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + bool revert3 = what != BAD() && what != TAU() && what != TOC(); + bool revert4 = what == BAD() && to_mathint(data) != 0 && to_mathint(data) != 1; + bool revert5 = what == TAU() && to_mathint(data) > max_uint64; + bool revert6 = what == TOC() && to_mathint(data) > max_uint128; + + file@withrevert(e, what, data); + + assert lastReverted <=> + revert1 || revert2 || revert3 || + revert4 || revert5 || revert6, + "file revert rules failed"; +} + +// Verify correct storage changes for non-reverting file for individual rate parameters +rule file_per_id(bytes32 id, bytes32 what, uint256 data) { + env e; + bytes32 other; + require other != id; + + mathint minBefore; mathint maxBefore; mathint stepBefore; + minBefore, maxBefore, stepBefore = cfgs(id); + + mathint minOtherBefore; mathint maxOtherBefore; mathint stepOtherBefore; + minOtherBefore, maxOtherBefore, stepOtherBefore = cfgs(other); + + file(e, id, what, data); + + mathint minAfter; mathint maxAfter; mathint stepAfter; + minAfter, maxAfter, stepAfter = cfgs(id); + + assert what == MIN() => minAfter == to_mathint(data), "file did not set min"; + assert what != MIN() => minAfter == minBefore, "file did keep unchanged min"; + assert what == MAX() => maxAfter == to_mathint(data), "file did not set max"; + assert what != MAX() => maxAfter == maxBefore, "file did keep unchanged max"; + assert what == STEP() => stepAfter == to_mathint(data), "file did not set step"; + assert what != STEP() => stepAfter == stepBefore, "file did keep unchanged step"; + + mathint minOtherAfter; mathint maxOtherAfter; mathint stepOtherAfter; + minOtherAfter, maxOtherAfter, stepOtherAfter = cfgs(other); + + assert minOtherAfter == minOtherBefore, "file unexpectedly changed other min"; + assert maxOtherAfter == maxOtherBefore, "file unexpectedly changed other max"; + assert stepOtherAfter == stepOtherBefore, "file unexpectedly changed other step"; +} + +// Verify revert rules on file for individual rate parameters +rule file_per_id_revert(bytes32 id, bytes32 what, uint256 data) { + env e; + + mathint wardsSender = wards(e.msg.sender); + mathint minBefore; mathint maxBefore; mathint stepBefore; + minBefore, maxBefore, stepBefore = cfgs(id); + mathint duty; mathint _rho; + duty, _rho = jug.ilks(id); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + bool revert3 = id != DSR() && id != SSR() && duty == 0; + bool revert4 = what != MIN() && what != MAX() && what != STEP(); + bool revert5 = to_mathint(data) > max_uint16; + bool revert6 = what == MIN() && to_mathint(data) > maxBefore; + bool revert7 = what == MAX() && to_mathint(data) < minBefore; + + file@withrevert(e, id, what, data); + + assert lastReverted <=> + revert1 || revert2 || revert3 || + revert4 || revert5 || revert6 || + revert7, + "file revert rules failed"; +} + +ghost mapping(mathint => mathint) bps_to_ray { + init_state axiom forall mathint i. bps_to_ray[i] == 0; +} + +// Verify correct storage changes for non-reverting set +rule set(SPBEAM.ParamChange[] updates) { + env e; + bytes32 ilk; + require ilk != DSR() && ilk != SSR(); + require updates.length < 4; + + mathint dsrBefore = pot.dsr(); + mathint ssrBefore = susds.ssr(); + mathint dutyBefore; mathint _rho; + dutyBefore, _rho = jug.ilks(ilk); + + set(e, updates); + + mathint dsrAfter = pot.dsr(); + mathint ssrAfter = susds.ssr(); + mathint dutyAfter; + dutyAfter, _rho = jug.ilks(ilk); + + // Manually convert all BPS values to RAY after the function call + // Store in the ghost mapping to use it in the assertions + if (updates.length > 0) { + bps_to_ray[updates[0].bps] = conv.btor(updates[0].bps); + } + if (updates.length > 1) { + bps_to_ray[updates[1].bps] = conv.btor(updates[1].bps); + } + if (updates.length > 2) { + bps_to_ray[updates[2].bps] = conv.btor(updates[2].bps); + } + + // If DSR is in updates, then its value should match the converted input value + assert exists uint256 i. i < updates.length && updates[i].id == DSR() => + dsrAfter == bps_to_ray[updates[i].bps], "DSR in updates; dsr not set correctly"; + // If DSR is not in updates, then the value should not change + assert (forall uint256 i. i < updates.length => updates[i].id != DSR()) => + dsrAfter == dsrBefore, "DSR not in updates; dsr changed unexpectedly"; + // If the value of DSR changed, then it should be in updates + assert dsrAfter != dsrBefore => + (exists uint256 i. i < updates.length && updates[i].id == DSR()), "dsr changed; DSR not in updates"; + // If the value of DSR did not change, then it should either NOT be in updates or be in updates with the same value + assert dsrAfter == dsrBefore => ( + (forall uint256 i. i < updates.length => updates[i].id != DSR()) || + (exists uint256 i. i < updates.length && updates[i].id == DSR() && bps_to_ray[updates[i].bps] == dsrBefore) + ), "dsr not changed; DSR in updates with different value"; + + // If SSR is in updates, then its value should match the converted input value + assert exists uint256 i. i < updates.length && updates[i].id == SSR() => + ssrAfter == bps_to_ray[updates[i].bps], "SSR in updates; ssr not set correctly"; + // If SSR is not in updates, then the value should not change + assert (forall uint256 i. i < updates.length => updates[i].id != SSR()) => + ssrAfter == ssrBefore, "SSR not in updates; ssr changed unexpectedly"; + // If the value of SSR changed, then it should be in updates + assert ssrAfter != ssrBefore => ( + exists uint256 i. i < updates.length && updates[i].id == SSR() + ), "ssr changed; SSR not in updates"; + // If the value of SSR did not change, then it should either NOT be in updates or be in updates with the same value + assert ssrAfter == ssrBefore => ( + (forall uint256 i. i < updates.length => updates[i].id != SSR()) || + (exists uint256 i. i < updates.length && updates[i].id == SSR() && bps_to_ray[updates[i].bps] == ssrBefore) + ), "ssr not changed; SSR in updates with different value"; + + // If ilk is in updates, then its duty value should match the converted input value + assert exists uint256 i. i < updates.length && updates[i].id == ilk => + dutyAfter == bps_to_ray[updates[i].bps], "ilk in updates; duty not set correctly"; + // If ilk is not in updates, then the value should not change + assert (forall uint256 i. i < updates.length => updates[i].id != ilk) => + dutyAfter == dutyBefore, "ilk not in updates; duty changed unexpectedly"; + // If the value of ilk duty changed, then it should be in updates + assert dutyAfter != dutyBefore => + (exists uint256 i. i < updates.length && updates[i].id == ilk), "duty changed; ilk not in updates"; + // If the value of ilk duty did not change, then it should either NOT be in updates or be in updates with the same value + assert dutyAfter == dutyBefore => ( + (forall uint256 i. i < updates.length => updates[i].id != ilk) || + (exists uint256 i. i < updates.length && updates[i].id == ilk && bps_to_ray[updates[i].bps] == dutyBefore) + ), "duty not changed; ilk in updates with different value"; +} + +ghost mapping(bytes32 => bool) set_item_reverted { + init_state axiom forall bytes32 i. set_item_reverted[i] == false; +} + +rule set_revert(SPBEAM.ParamChange[] updates, uint256[] idsAsUints) { + env e; + bytes32 ilk; + + require ilk != DSR() && ilk != SSR(); + require updates.length < 4; + require updates.length == idsAsUints.length; + require forall uint256 i. i < updates.length => ( + // ID cannot be bytes32(0) + updates[i].id != EMPTY_BYTES32() && + updates[i].id == to_bytes32(idsAsUints[i]) + ); + // It is impossible for `toc` to be greater than block.timestamp + require toc() <= e.block.timestamp; + // Required because `toc` is a `uint128` and `toc = block.timestamp` in the implementation + require e.block.timestamp <= max_uint128; + + bool revert1 = e.msg.value > 0; + bool revert2 = buds(e.msg.sender) != 1; + bool revert3 = bad() != 0; + bool revert4 = e.block.timestamp < tau() + toc(); + // No updates + bool revert5 = updates.length == 0; + // Strictly ordered elements + bool revert6 = updates.length == 2 ? idsAsUints[0] >= idsAsUints[1] : false; + bool revert7 = updates.length == 3 ? ( + idsAsUints[0] >= idsAsUints[1] || + idsAsUints[1] >= idsAsUints[2] + ) : false; + + // Check if any update would revert + if (updates.length > 0) { + set_item_reverted[updates[0].id] = check_item_revert(e, updates[0].id, updates[0].bps); + } + if (updates.length > 1) { + set_item_reverted[updates[1].id] = check_item_revert(e, updates[1].id, updates[1].bps); + } + if (updates.length > 2) { + set_item_reverted[updates[2].id] = check_item_revert(e, updates[2].id, updates[2].bps); + } + bool revert8 = exists uint256 i. i < updates.length && set_item_reverted[updates[i].id]; + + set@withrevert(e, updates); + + assert lastReverted => + revert1 || revert2 || revert3 || + revert4 || revert5 || revert6 || + revert7 || revert8, + "set reverted for an unknown reason"; + + assert revert1 || revert2 || revert3 || + revert4 || revert5 || revert6 || + revert7 || revert8 => + lastReverted, + "set should have reverted"; +} + +function abs_diff(mathint a, mathint b) returns mathint { + return a > b ? a - b : b - a; +} + +function check_item_revert(env e, bytes32 id, uint256 bps) returns bool { + mathint min; mathint max; mathint step; + min, max, step = cfgs(id); + + // min <= max is enforced in the implementation + require min <= max; + + mathint oldBps; + if (id == DSR()) { + // IF block.timestamp < rho, drip will revert + require(e.block.timestamp >= pot.rho()); + oldBps = conv.rtob(pot.dsr()); + } else if (id == SSR()) { + // IF block.timestamp <= rho, drip will not update rho and file will revert + require(e.block.timestamp > pot.rho()); + oldBps = conv.rtob(susds.ssr()); + } else { + uint256 duty; mathint rho; + duty, rho = jug.ilks(id); + // IF rho >= block.timestamp, drip will revert + require(e.block.timestamp >= rho); + oldBps = conv.rtob(duty); + } + + // We need a second variable because it's not possible to reassign variables in CVL + mathint actualOldBps; + if (oldBps < min) { + actualOldBps = min; + } else if (oldBps > max) { + actualOldBps = max; + } else { + actualOldBps = oldBps; + } + + mathint delta = abs_diff(bps, actualOldBps); + mathint ray = conv.btor(bps); + + bool revertA = step == 0; + bool revertB = to_mathint(bps) > max; + bool revertC = to_mathint(bps) < min; + bool revertD = delta > step; + bool revertE = ray < RAY(); + bool revertF = bps > conv.MAX_BPS_IN(); + bool revertG = id == DSR() && pot.wards(currentContract) != 1; + bool revertH = id == SSR() && susds.wards(currentContract) != 1; + bool revertI = id != DSR() && id != SSR() && jug.wards(currentContract) != 1; + + return + revertA || revertB || revertC || + revertD || revertE || revertF || + revertG || revertH || revertI; +} + +rule set_invariants_current_within_bounds(SPBEAM.ParamChange[] updates) { + env e; + + require updates.length == 1; + bytes32 id = updates[0].id; + uint256 bps = updates[0].bps; + + bytes32 ilk; + require ilk != DSR() && ilk != SSR(); + + mathint min; mathint max; mathint step; + min, max, step = cfgs(id); + + uint256 dsrBefore = pot.dsr(); + uint256 ssrBefore = susds.ssr(); + uint256 dutyBefore; uint256 _rho; + dutyBefore, _rho = jug.ilks(ilk); + + mathint dsrBeforeBps = conv.rtob(dsrBefore); + mathint ssrBeforeBps = conv.rtob(ssrBefore); + mathint dutyBeforeBps = conv.rtob(dutyBefore); + + // Ensure the previous values are within bounds + require id == DSR() => dsrBeforeBps >= min && dsrBeforeBps <= max; + require id == SSR() => ssrBeforeBps >= min && ssrBeforeBps <= max; + require id == ilk => dutyBeforeBps >= min && dutyBeforeBps <= max; + + set(e, updates); + + uint256 dsrAfter = pot.dsr(); + uint256 ssrAfter = susds.ssr(); + uint256 dutyAfter; + dutyAfter, _rho = jug.ilks(ilk); + + mathint dsrAfterBps = conv.rtob(dsrAfter); + mathint ssrAfterBps = conv.rtob(ssrAfter); + mathint dutyAfterBps = conv.rtob(dutyAfter); + + // Set cannot set the value of the rate greater than max + assert id == DSR() => dsrAfterBps <= max, "dsrAfterBps > max"; + // Set cannot set the value of the rate lower than min + assert id == DSR() => dsrAfterBps >= min, "dsrAfterBps < min"; + // Set cannot set the value of the rate to change by more than step + assert id == DSR() => abs_diff(dsrAfterBps, dsrBeforeBps) <= step, "abs(dsrAfterBps - dsrBeforeBps) > step"; + + // Set cannot set the value of the rate greater than max + assert id == SSR() => ssrAfterBps <= max, "ssrAfterBps > max"; + // Set cannot set the value of the rate lower than min + assert id == SSR() => ssrAfterBps >= min, "ssrAfterBps < min"; + // Set cannot set the value of the rate to change by more than step + assert id == SSR() => abs_diff(ssrAfterBps, ssrBeforeBps) <= step, "abs(ssrAfterBps - ssrBeforeBps) > step"; + + // Set cannot set the value of the rate greater than max + assert id == ilk => dutyAfterBps <= max, "dutyAfterBps > max"; + // Set cannot set the value of the rate lower than min + assert id == ilk => dutyAfterBps >= min, "dutyAfterBps < min"; + // Set cannot set the value of the rate to change by more than step + assert id == ilk => abs_diff(dutyAfterBps, dutyBeforeBps) <= step, "abs(dutyAfterBps - dutyBeforeBps) > step"; +} + +rule set_invariants_current_higher_than_max(SPBEAM.ParamChange[] updates) { + env e; + + require updates.length == 1; + bytes32 id = updates[0].id; + uint256 bps = updates[0].bps; + + bytes32 ilk; + require ilk != DSR() && ilk != SSR(); + + uint256 min; uint256 max; uint256 step; + min, max, step = cfgs(id); + require bps >= min && bps <= max; + bps_to_ray[bps] = conv.btor(bps); + bps_to_ray[max] = conv.btor(max); + + uint256 dsrBefore = pot.dsr(); + uint256 ssrBefore = susds.ssr(); + uint256 dutyBefore; mathint _rho; + dutyBefore, _rho = jug.ilks(ilk); + + require id == DSR() => dsrBefore > bps_to_ray[max]; + require id == SSR() => ssrBefore > bps_to_ray[max]; + require id != DSR() && id != SSR() => dutyBefore > bps_to_ray[max]; + + set(e, updates); + + uint256 dsrAfter = pot.dsr(); + uint256 ssrAfter = susds.ssr(); + uint256 dutyAfter; + dutyAfter, _rho = jug.ilks(ilk); + + assert id == DSR() => dsrAfter == bps_to_ray[bps] && bps >= max - step && bps <= max, "dsr not within bounds"; + assert id == SSR() => ssrAfter == bps_to_ray[bps] && bps >= max - step && bps <= max, "ssr not within bounds"; + assert id == ilk => dutyAfter == bps_to_ray[bps] && bps >= max - step && bps <= max, "ilk duty not within bounds"; +} + +rule set_invariants_current_lower_than_min(SPBEAM.ParamChange[] updates) { + env e; + + require updates.length == 1; + bytes32 id = updates[0].id; + uint256 bps = updates[0].bps; + + bytes32 ilk; + require ilk != DSR() && ilk != SSR(); + + uint256 min; uint256 max; uint256 step; + min, max, step = cfgs(id); + require bps >= min && bps <= max; + bps_to_ray[bps] = conv.btor(bps); + bps_to_ray[min] = conv.btor(min); + + uint256 dsrBefore = pot.dsr(); + uint256 ssrBefore = susds.ssr(); + uint256 dutyBefore; mathint _rho; + dutyBefore, _rho = jug.ilks(ilk); + + require id == DSR() => dsrBefore < bps_to_ray[min]; + require id == SSR() => ssrBefore < bps_to_ray[min]; + require id != DSR() && id != SSR() => dutyBefore < bps_to_ray[min]; + + set(e, updates); + + uint256 dsrAfter = pot.dsr(); + uint256 ssrAfter = susds.ssr(); + uint256 dutyAfter; + dutyAfter, _rho = jug.ilks(ilk); + + assert id == DSR() => dsrAfter == bps_to_ray[bps] && bps >= min && bps <= min + step, "dsr not within bounds"; + assert id == SSR() => ssrAfter == bps_to_ray[bps] && bps >= min && bps <= min + step, "ssr not within bounds"; + assert id == ilk => dutyAfter == bps_to_ray[bps] && bps >= min && bps <= min + step, "ilk duty not within bounds"; +} diff --git a/certora/mocks/Conv.sol b/certora/mocks/Conv.sol new file mode 100644 index 0000000..ba209b4 --- /dev/null +++ b/certora/mocks/Conv.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.8.24; + +contract Conv { + uint256 public constant MAX_BPS_IN = 50_00; + uint256 internal constant RAY = 10 ** 27; + uint256 internal constant BPS = 100_00; + + function btor(uint256 bps) external view returns (uint256 ray) { + require(bps <= MAX_BPS_IN, "Conv/bps-too-high"); + + // Deliberately wrong implementation + return (bps * RAY + BPS / 2) / BPS / 365 days + RAY; + } + + function rtob(uint256 ray) external pure returns (uint256 bps) { + require(ray >= RAY, "Conv/ray-too-low"); + + // Deliberately wrong implementation + return ((ray - RAY) * BPS * 365 days + RAY / 2) / RAY; + } +} diff --git a/certora/mocks/Jug.sol b/certora/mocks/Jug.sol new file mode 100644 index 0000000..146d67d --- /dev/null +++ b/certora/mocks/Jug.sol @@ -0,0 +1,185 @@ +/** + * Submitted for verification at Etherscan.io on 2019-11-14 + */ + +// hevm: flattened sources of /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/jug.sol +pragma solidity =0.5.12; + +////// /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/lib.sol +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/* pragma solidity 0.5.12; */ + +contract LibNote { + event LogNote( + bytes4 indexed sig, address indexed usr, bytes32 indexed arg1, bytes32 indexed arg2, bytes data + ) anonymous; + + modifier note( // end of memory ensures zero + ) { + _; + assembly { + // log an 'anonymous' event with a constant 6 words of calldata + // and four indexed topics: selector, caller, arg1 and arg2 + let mark := msize + mstore(0x40, add(mark, 288)) // update free memory pointer + mstore(mark, 0x20) // bytes type data offset + mstore(add(mark, 0x20), 224) // bytes size (padded) + calldatacopy(add(mark, 0x40), 0, 224) // bytes payload + log4( + mark, + 288, // calldata + shl(224, shr(224, calldataload(0))), // msg.sig + caller, // msg.sender + calldataload(4), // arg1 + calldataload(36) // arg2 + ) + } + } +} + +////// /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/jug.sol +/* pragma solidity 0.5.12; */ + +/* import "./lib.sol"; */ + +contract VatLike { + function ilks(bytes32) + external + returns ( + uint256 Art, // wad + uint256 rate + ); // ray + + function fold(bytes32, address, int256) external; +} + +contract Jug is LibNote { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address usr) external note auth { + wards[usr] = 1; + } + + function deny(address usr) external note auth { + wards[usr] = 0; + } + + modifier auth() { + require(wards[msg.sender] == 1, "Jug/not-authorized"); + _; + } + + // --- Data --- + struct Ilk { + uint256 duty; + uint256 rho; + } + + mapping(bytes32 => Ilk) public ilks; + VatLike public vat; + address public vow; + uint256 public base; + + // --- Init --- + constructor(address vat_) public { + wards[msg.sender] = 1; + vat = VatLike(vat_); + } + + // --- Math --- + function rpow(uint256 x, uint256 n, uint256 b) internal pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { z := b } + default { z := 0 } + } + default { + switch mod(n, 2) + case 0 { z := b } + default { z := x } + let half := div(b, 2) // for rounding. + for { n := div(n, 2) } n { n := div(n, 2) } { + let xx := mul(x, x) + if iszero(eq(div(xx, x), x)) { revert(0, 0) } + let xxRound := add(xx, half) + if lt(xxRound, xx) { revert(0, 0) } + x := div(xxRound, b) + if mod(n, 2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { revert(0, 0) } + let zxRound := add(zx, half) + if lt(zxRound, zx) { revert(0, 0) } + z := div(zxRound, b) + } + } + } + } + } + + uint256 constant ONE = 10 ** 27; + + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x + y; + require(z >= x); + } + + function diff(uint256 x, uint256 y) internal pure returns (int256 z) { + z = int256(x) - int256(y); + require(int256(x) >= 0 && int256(y) >= 0); + } + + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = x * y; + require(y == 0 || z / y == x); + z = z / ONE; + } + + // --- Administration --- + function init(bytes32 ilk) external note { + Ilk storage i = ilks[ilk]; + require(i.duty == 0, "Jug/ilk-already-init"); + i.duty = ONE; + i.rho = now; + } + + function file(bytes32 ilk, bytes32 what, uint256 data) external auth note { + require(now == ilks[ilk].rho, "Jug/rho-not-updated"); + if (what == "duty") ilks[ilk].duty = data; + else revert("Jug/file-unrecognized-param"); + } + + function file(bytes32 what, uint256 data) external auth note { + if (what == "base") base = data; + else revert("Jug/file-unrecognized-param"); + } + + function file(bytes32 what, address data) external auth note { + if (what == "vow") vow = data; + else revert("Jug/file-unrecognized-param"); + } + + // --- Stability Fee Collection --- + function drip(bytes32 ilk) external note returns (uint256 rate) { + require(now >= ilks[ilk].rho, "Jug/invalid-now"); + // (, uint prev) = vat.ilks(ilk); + // Note: ignoring rpow for Certora + // rate = rmul(rpow(add(base, ilks[ilk].duty), now - ilks[ilk].rho, ONE), prev); + // vat.fold(ilk, vow, diff(rate, prev)); + ilks[ilk].rho = now; + } +} diff --git a/certora/mocks/Pot.sol b/certora/mocks/Pot.sol new file mode 100644 index 0000000..a1218c8 --- /dev/null +++ b/certora/mocks/Pot.sol @@ -0,0 +1,229 @@ +/** + * Submitted for verification at Etherscan.io on 2019-11-14 + */ + +// hevm: flattened sources of /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/pot.sol +pragma solidity =0.5.12; + +////// /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/lib.sol +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/* pragma solidity 0.5.12; */ + +contract LibNote { + event LogNote( + bytes4 indexed sig, address indexed usr, bytes32 indexed arg1, bytes32 indexed arg2, bytes data + ) anonymous; + + modifier note( // end of memory ensures zero + ) { + _; + assembly { + // log an 'anonymous' event with a constant 6 words of calldata + // and four indexed topics: selector, caller, arg1 and arg2 + let mark := msize + mstore(0x40, add(mark, 288)) // update free memory pointer + mstore(mark, 0x20) // bytes type data offset + mstore(add(mark, 0x20), 224) // bytes size (padded) + calldatacopy(add(mark, 0x40), 0, 224) // bytes payload + log4( + mark, + 288, // calldata + shl(224, shr(224, calldataload(0))), // msg.sig + caller, // msg.sender + calldataload(4), // arg1 + calldataload(36) // arg2 + ) + } + } +} + +////// /nix/store/8xb41r4qd0cjb63wcrxf1qmfg88p0961-dss-6fd7de0/src/pot.sol +/// pot.sol -- Dai Savings Rate + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/* pragma solidity 0.5.12; */ + +/* import "./lib.sol"; */ + +/* + "Savings Dai" is obtained when Dai is deposited into + this contract. Each "Savings Dai" accrues Dai interest + at the "Dai Savings Rate". + + This contract does not implement a user tradeable token + and is intended to be used with adapters. + + --- `save` your `dai` in the `pot` --- + + - `dsr`: the Dai Savings Rate + - `pie`: user balance of Savings Dai + + - `join`: start saving some dai + - `exit`: remove some dai + - `drip`: perform rate collection + +*/ + +contract VatLike { + function move(address, address, uint256) external; + function suck(address, address, uint256) external; +} + +contract Pot is LibNote { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address guy) external note auth { + wards[guy] = 1; + } + + function deny(address guy) external note auth { + wards[guy] = 0; + } + + modifier auth() { + require(wards[msg.sender] == 1, "Pot/not-authorized"); + _; + } + + // --- Data --- + mapping(address => uint256) public pie; // user Savings Dai + + uint256 public Pie; // total Savings Dai + uint256 public dsr; // the Dai Savings Rate + uint256 public chi; // the Rate Accumulator + + VatLike public vat; // CDP engine + address public vow; // debt engine + uint256 public rho; // time of last drip + + uint256 public live; // Access Flag + + // --- Init --- + constructor(address vat_) public { + wards[msg.sender] = 1; + vat = VatLike(vat_); + dsr = ONE; + chi = ONE; + rho = now; + live = 1; + } + + // --- Math --- + uint256 constant ONE = 10 ** 27; + + function rpow(uint256 x, uint256 n, uint256 base) internal pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { z := base } + default { z := 0 } + } + default { + switch mod(n, 2) + case 0 { z := base } + default { z := x } + let half := div(base, 2) // for rounding. + for { n := div(n, 2) } n { n := div(n, 2) } { + let xx := mul(x, x) + if iszero(eq(div(xx, x), x)) { revert(0, 0) } + let xxRound := add(xx, half) + if lt(xxRound, xx) { revert(0, 0) } + x := div(xxRound, base) + if mod(n, 2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { revert(0, 0) } + let zxRound := add(zx, half) + if lt(zxRound, zx) { revert(0, 0) } + z := div(zxRound, base) + } + } + } + } + } + + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = mul(x, y) / ONE; + } + + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + // --- Administration --- + function file(bytes32 what, uint256 data) external auth note { + require(now == rho, "Pot/rho-not-updated"); + if (what == "dsr") dsr = data; + else revert("Pot/file-unrecognized-param"); + } + + function file(bytes32 what, address addr) external auth note { + if (what == "vow") vow = addr; + else revert("Pot/file-unrecognized-param"); + } + + function cage() external note { + live = 0; + dsr = ONE; + } + + // --- Savings Rate Accumulation --- + function drip() external note returns (uint256 tmp) { + require(now >= rho, "Pot/invalid-now"); + // Note: ignoring rpow for Certora + // tmp = rmul(rpow(dsr, now - rho, ONE), chi); + // uint chi_ = sub(tmp, chi); + // chi = tmp; + rho = now; + // vat.suck(address(vow), address(this), mul(Pie, chi)); + } + + // --- Savings Dai Management --- + function join(uint256 wad) external note { + require(now == rho, "Pot/rho-not-updated"); + pie[msg.sender] = add(pie[msg.sender], wad); + Pie = add(Pie, wad); + vat.move(msg.sender, address(this), mul(chi, wad)); + } + + function exit(uint256 wad) external note { + pie[msg.sender] = sub(pie[msg.sender], wad); + Pie = sub(Pie, wad); + vat.move(address(this), msg.sender, mul(chi, wad)); + } +} diff --git a/certora/mocks/SUsds.sol b/certora/mocks/SUsds.sol new file mode 100644 index 0000000..a448a0b --- /dev/null +++ b/certora/mocks/SUsds.sol @@ -0,0 +1,1357 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.20 ^0.8.21; + +// lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol + +// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol) + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be + * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in + * case an upgrade adds a module that needs to be initialized. + * + * For example: + * + * [.hljs-theme-light.nopadding] + * ```solidity + * contract MyToken is ERC20Upgradeable { + * function initialize() initializer public { + * __ERC20_init("MyToken", "MTK"); + * } + * } + * + * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { + * function initializeV2() reinitializer(2) public { + * __ERC20Permit_init("MyToken"); + * } + * } + * ``` + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + * + * [CAUTION] + * ==== + * Avoid leaving a contract uninitialized. + * + * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation + * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke + * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: + * + * [.hljs-theme-light.nopadding] + * ``` + * /// @custom:oz-upgrades-unsafe-allow constructor + * constructor() { + * _disableInitializers(); + * } + * ``` + * ==== + */ +abstract contract Initializable { + /** + * @dev Storage of the initializable contract. + * + * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions + * when using with upgradeable contracts. + * + * @custom:storage-location erc7201:openzeppelin.storage.Initializable + */ + struct InitializableStorage { + /** + * @dev Indicates that the contract has been initialized. + */ + uint64 _initialized; + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool _initializing; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + /** + * @dev The contract is already initialized. + */ + error InvalidInitialization(); + + /** + * @dev The contract is not initializing. + */ + error NotInitializing(); + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint64 version); + + /** + * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, + * `onlyInitializing` functions can be used to initialize parent contracts. + * + * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any + * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in + * production. + * + * Emits an {Initialized} event. + */ + modifier initializer() { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + // Cache values to avoid duplicated sloads + bool isTopLevelCall = !$._initializing; + uint64 initialized = $._initialized; + + // Allowed calls: + // - initialSetup: the contract is not in the initializing state and no previous version was + // initialized + // - construction: the contract is initialized at version 1 (no reininitialization) and the + // current contract is just being deployed + bool initialSetup = initialized == 0 && isTopLevelCall; + bool construction = initialized == 1 && address(this).code.length == 0; + + if (!initialSetup && !construction) { + revert InvalidInitialization(); + } + $._initialized = 1; + if (isTopLevelCall) { + $._initializing = true; + } + _; + if (isTopLevelCall) { + $._initializing = false; + emit Initialized(1); + } + } + + /** + * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the + * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be + * used to initialize parent contracts. + * + * A reinitializer may be used after the original initialization step. This is essential to configure modules that + * are added through upgrades and that require initialization. + * + * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` + * cannot be nested. If one is invoked in the context of another, execution will revert. + * + * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in + * a contract, executing them in the right order is up to the developer or operator. + * + * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization. + * + * Emits an {Initialized} event. + */ + modifier reinitializer(uint64 version) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing || $._initialized >= version) { + revert InvalidInitialization(); + } + $._initialized = version; + $._initializing = true; + _; + $._initializing = false; + emit Initialized(version); + } + + /** + * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the + * {initializer} and {reinitializer} modifiers, directly or indirectly. + */ + modifier onlyInitializing() { + _checkInitializing(); + _; + } + + /** + * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. + */ + function _checkInitializing() internal view virtual { + if (!_isInitializing()) { + revert NotInitializing(); + } + } + + /** + * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. + * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized + * to any version. It is recommended to use this to lock implementation contracts that are designed to be called + * through proxies. + * + * Emits an {Initialized} event the first time it is successfully executed. + */ + function _disableInitializers() internal virtual { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing) { + revert InvalidInitialization(); + } + if ($._initialized != type(uint64).max) { + $._initialized = type(uint64).max; + emit Initialized(type(uint64).max); + } + } + + /** + * @dev Returns the highest version that has been initialized. See {reinitializer}. + */ + function _getInitializedVersion() internal view returns (uint64) { + return _getInitializableStorage()._initialized; + } + + /** + * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. + */ + function _isInitializing() internal view returns (bool) { + return _getInitializableStorage()._initializing; + } + + /** + * @dev Returns a pointer to the storage namespace. + */ + // solhint-disable-next-line var-name-mixedcase + function _getInitializableStorage() private pure returns (InitializableStorage storage $) { + assembly { + $.slot := INITIALIZABLE_STORAGE + } + } +} + +// lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol + +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC1822.sol) + +/** + * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified + * proxy whose upgrades are fully controlled by the current implementation. + */ +interface IERC1822Proxiable { + /** + * @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation + * address. + * + * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks + * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this + * function revert if invoked through a proxy. + */ + function proxiableUUID() external view returns (bytes32); +} + +// lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol + +// OpenZeppelin Contracts (last updated v5.0.0) (proxy/beacon/IBeacon.sol) + +/** + * @dev This is the interface that {BeaconProxy} expects of its beacon. + */ +interface IBeacon { + /** + * @dev Must return an address that can be used as a delegate call target. + * + * {UpgradeableBeacon} will check that this address is a contract. + */ + function implementation() external view returns (address); +} + +// lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol + +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev The ETH balance of the account is not enough to perform the operation. + */ + error AddressInsufficientBalance(address account); + + /** + * @dev There's no code at `target` (it is not a contract). + */ + error AddressEmptyCode(address target); + + /** + * @dev A call to an address target failed. The target may have reverted. + */ + error FailedInnerCall(); + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + if (address(this).balance < amount) { + revert AddressInsufficientBalance(address(this)); + } + + (bool success,) = recipient.call{value: amount}(""); + if (!success) { + revert FailedInnerCall(); + } + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason or custom error, it is bubbled + * up by this function (like regular Solidity function calls). However, if + * the call reverted with no returned reason, this function reverts with a + * {FailedInnerCall} error. + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + if (address(this).balance < value) { + revert AddressInsufficientBalance(address(this)); + } + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResultFromTarget(target, success, returndata); + } + + /** + * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target + * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an + * unsuccessful call. + */ + function verifyCallResultFromTarget(address target, bool success, bytes memory returndata) + internal + view + returns (bytes memory) + { + if (!success) { + _revert(returndata); + } else { + // only check if target is a contract if the call was successful and the return data is empty + // otherwise we already know that it was a contract + if (returndata.length == 0 && target.code.length == 0) { + revert AddressEmptyCode(target); + } + return returndata; + } + } + + /** + * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the + * revert reason or with a default {FailedInnerCall} error. + */ + function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { + if (!success) { + _revert(returndata); + } else { + return returndata; + } + } + + /** + * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. + */ + function _revert(bytes memory returndata) private pure { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert FailedInnerCall(); + } + } +} + +// lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol + +// OpenZeppelin Contracts (last updated v5.0.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * ```solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(newImplementation.code.length > 0); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } +} + +// lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol + +// OpenZeppelin Contracts (last updated v5.0.0) (proxy/ERC1967/ERC1967Utils.sol) + +/** + * @dev This abstract contract provides getters and event emitting update functions for + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. + */ +library ERC1967Utils { + // We re-declare ERC-1967 events here because they can't be used directly from IERC1967. + // This will be fixed in Solidity 0.8.21. At that point we should remove these events. + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Emitted when the beacon is changed. + */ + event BeaconUpgraded(address indexed beacon); + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev The `implementation` of the proxy is invalid. + */ + error ERC1967InvalidImplementation(address implementation); + + /** + * @dev The `admin` of the proxy is invalid. + */ + error ERC1967InvalidAdmin(address admin); + + /** + * @dev The `beacon` of the proxy is invalid. + */ + error ERC1967InvalidBeacon(address beacon); + + /** + * @dev An upgrade function sees `msg.value > 0` that may be lost. + */ + error ERC1967NonPayable(); + + /** + * @dev Returns the current implementation address. + */ + function getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 implementation slot. + */ + function _setImplementation(address newImplementation) private { + if (newImplementation.code.length == 0) { + revert ERC1967InvalidImplementation(newImplementation); + } + StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation; + } + + /** + * @dev Performs implementation upgrade with additional setup call if data is nonempty. + * This function is payable only if the setup call is performed, otherwise `msg.value` is rejected + * to avoid stuck value in the contract. + * + * Emits an {IERC1967-Upgraded} event. + */ + function upgradeToAndCall(address newImplementation, bytes memory data) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + + if (data.length > 0) { + Address.functionDelegateCall(newImplementation, data); + } else { + _checkNonPayable(); + } + } + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Returns the current admin. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using + * the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + */ + function getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(ADMIN_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 admin slot. + */ + function _setAdmin(address newAdmin) private { + if (newAdmin == address(0)) { + revert ERC1967InvalidAdmin(address(0)); + } + StorageSlot.getAddressSlot(ADMIN_SLOT).value = newAdmin; + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {IERC1967-AdminChanged} event. + */ + function changeAdmin(address newAdmin) internal { + emit AdminChanged(getAdmin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. + * This is the keccak-256 hash of "eip1967.proxy.beacon" subtracted by 1. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + /** + * @dev Returns the current beacon. + */ + function getBeacon() internal view returns (address) { + return StorageSlot.getAddressSlot(BEACON_SLOT).value; + } + + /** + * @dev Stores a new beacon in the EIP1967 beacon slot. + */ + function _setBeacon(address newBeacon) private { + if (newBeacon.code.length == 0) { + revert ERC1967InvalidBeacon(newBeacon); + } + + StorageSlot.getAddressSlot(BEACON_SLOT).value = newBeacon; + + address beaconImplementation = IBeacon(newBeacon).implementation(); + if (beaconImplementation.code.length == 0) { + revert ERC1967InvalidImplementation(beaconImplementation); + } + } + + /** + * @dev Change the beacon and trigger a setup call if data is nonempty. + * This function is payable only if the setup call is performed, otherwise `msg.value` is rejected + * to avoid stuck value in the contract. + * + * Emits an {IERC1967-BeaconUpgraded} event. + * + * CAUTION: Invoking this function has no effect on an instance of {BeaconProxy} since v5, since + * it uses an immutable beacon without looking at the value of the ERC-1967 beacon slot for + * efficiency. + */ + function upgradeBeaconToAndCall(address newBeacon, bytes memory data) internal { + _setBeacon(newBeacon); + emit BeaconUpgraded(newBeacon); + + if (data.length > 0) { + Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); + } else { + _checkNonPayable(); + } + } + + /** + * @dev Reverts if `msg.value` is not zero. It can be used to avoid `msg.value` stuck in the contract + * if an upgrade doesn't perform an initialization call. + */ + function _checkNonPayable() private { + if (msg.value > 0) { + revert ERC1967NonPayable(); + } + } +} + +// lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol + +// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/UUPSUpgradeable.sol) + +/** + * @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an + * {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy. + * + * A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is + * reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing + * `UUPSUpgradeable` with a custom implementation of upgrades. + * + * The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism. + */ +abstract contract UUPSUpgradeable is Initializable, IERC1822Proxiable { + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address private immutable __self = address(this); + + /** + * @dev The version of the upgrade interface of the contract. If this getter is missing, both `upgradeTo(address)` + * and `upgradeToAndCall(address,bytes)` are present, and `upgradeTo` must be used if no function should be called, + * while `upgradeToAndCall` will invoke the `receive` function if the second argument is the empty byte string. + * If the getter returns `"5.0.0"`, only `upgradeToAndCall(address,bytes)` is present, and the second argument must + * be the empty byte string if no function should be called, making it impossible to invoke the `receive` function + * during an upgrade. + */ + string public constant UPGRADE_INTERFACE_VERSION = "5.0.0"; + + /** + * @dev The call is from an unauthorized context. + */ + error UUPSUnauthorizedCallContext(); + + /** + * @dev The storage `slot` is unsupported as a UUID. + */ + error UUPSUnsupportedProxiableUUID(bytes32 slot); + + /** + * @dev Check that the execution is being performed through a delegatecall call and that the execution context is + * a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case + * for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a + * function through ERC1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to + * fail. + */ + modifier onlyProxy() { + _checkProxy(); + _; + } + + /** + * @dev Check that the execution is not being performed through a delegate call. This allows a function to be + * callable on the implementing contract but not through proxies. + */ + modifier notDelegated() { + _checkNotDelegated(); + _; + } + + function __UUPSUpgradeable_init() internal onlyInitializing {} + + function __UUPSUpgradeable_init_unchained() internal onlyInitializing {} + /** + * @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the + * implementation. It is used to validate the implementation's compatibility when performing an upgrade. + * + * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks + * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this + * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier. + */ + + function proxiableUUID() external view virtual notDelegated returns (bytes32) { + return ERC1967Utils.IMPLEMENTATION_SLOT; + } + + /** + * @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call + * encoded in `data`. + * + * Calls {_authorizeUpgrade}. + * + * Emits an {Upgraded} event. + * + * @custom:oz-upgrades-unsafe-allow-reachable delegatecall + */ + function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy { + _authorizeUpgrade(newImplementation); + _upgradeToAndCallUUPS(newImplementation, data); + } + + /** + * @dev Reverts if the execution is not performed via delegatecall or the execution + * context is not of a proxy with an ERC1967-compliant implementation pointing to self. + * See {_onlyProxy}. + */ + function _checkProxy() internal view virtual { + if ( + address(this) == __self // Must be called through delegatecall + || ERC1967Utils.getImplementation() != __self // Must be called through an active proxy + ) { + revert UUPSUnauthorizedCallContext(); + } + } + + /** + * @dev Reverts if the execution is performed via delegatecall. + * See {notDelegated}. + */ + function _checkNotDelegated() internal view virtual { + if (address(this) != __self) { + // Must not be called through delegatecall + revert UUPSUnauthorizedCallContext(); + } + } + + /** + * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by + * {upgradeToAndCall}. + * + * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}. + * + * ```solidity + * function _authorizeUpgrade(address) internal onlyOwner {} + * ``` + */ + function _authorizeUpgrade(address newImplementation) internal virtual; + + /** + * @dev Performs an implementation upgrade with a security check for UUPS proxies, and additional setup call. + * + * As a security check, {proxiableUUID} is invoked in the new implementation, and the return value + * is expected to be the implementation slot in ERC1967. + * + * Emits an {IERC1967-Upgraded} event. + */ + function _upgradeToAndCallUUPS(address newImplementation, bytes memory data) private { + try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { + if (slot != ERC1967Utils.IMPLEMENTATION_SLOT) { + revert UUPSUnsupportedProxiableUUID(slot); + } + ERC1967Utils.upgradeToAndCall(newImplementation, data); + } catch { + // The implementation is not UUPS + revert ERC1967Utils.ERC1967InvalidImplementation(newImplementation); + } + } +} + +// src/SUsds.sol + +/// SUsds.sol + +// Copyright (C) 2017, 2018, 2019 dbrock, rain, mrchico +// Copyright (C) 2021 Dai Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +interface IERC1271 { + function isValidSignature(bytes32, bytes memory) external view returns (bytes4); +} + +interface VatLike { + function hope(address) external; + function suck(address, address, uint256) external; +} + +interface UsdsJoinLike { + function vat() external view returns (address); + function usds() external view returns (address); + function exit(address, uint256) external; +} + +interface UsdsLike { + function transfer(address, uint256) external; + function transferFrom(address, address, uint256) external; +} + +contract SUsds is UUPSUpgradeable { + // --- Storage Variables --- + + // Admin + mapping(address => uint256) public wards; + // ERC20 + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + mapping(address => uint256) public nonces; + // Savings yield + uint256 public chi; // The Rate Accumulator [ray] + uint256 public rho; // Time of last drip [unix epoch time] + uint256 public ssr; // The USDS Savings Rate [ray] + + // --- Constants --- + + // ERC20 + string public constant name = "Savings USDS"; + string public constant symbol = "sUSDS"; + string public constant version = "1"; + uint8 public constant decimals = 18; + // Math + uint256 private constant RAY = 10 ** 27; + + // --- Immutables --- + + // EIP712 + bytes32 public constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + // Savings yield + UsdsJoinLike public immutable usdsJoin; + VatLike public immutable vat; + UsdsLike public immutable usds; + address public immutable vow; + + // --- Events --- + + // Admin + event Rely(address indexed usr); + event Deny(address indexed usr); + event File(bytes32 indexed what, uint256 data); + // ERC20 + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); + // ERC4626 + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + event Withdraw( + address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares + ); + // Referral + event Referral(uint16 indexed referral, address indexed owner, uint256 assets, uint256 shares); + // Savings yield + event Drip(uint256 chi, uint256 diff); + + // --- Modifiers --- + + modifier auth() { + require(wards[msg.sender] == 1, "SUsds/not-authorized"); + _; + } + + // --- Constructor --- + + constructor(address usdsJoin_, address vow_) { + _disableInitializers(); // Avoid initializing in the context of the implementation + + usdsJoin = UsdsJoinLike(usdsJoin_); + vat = VatLike(UsdsJoinLike(usdsJoin_).vat()); + usds = UsdsLike(UsdsJoinLike(usdsJoin_).usds()); + vow = vow_; + } + + // --- Upgradability --- + + function initialize() external initializer { + __UUPSUpgradeable_init(); + + chi = RAY; + rho = block.timestamp; + ssr = RAY; + vat.hope(address(usdsJoin)); + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + function _authorizeUpgrade(address newImplementation) internal override {} + + function getImplementation() external view returns (address) { + return ERC1967Utils.getImplementation(); + } + + // --- Internals --- + + // EIP712 + + function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainId, + address(this) + ) + ); + } + + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _calculateDomainSeparator(block.chainid); + } + + // Math + + function _rpow(uint256 x, uint256 n) internal pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { z := RAY } + default { z := 0 } + } + default { + switch mod(n, 2) + case 0 { z := RAY } + default { z := x } + let half := div(RAY, 2) // for rounding. + for { n := div(n, 2) } n { n := div(n, 2) } { + let xx := mul(x, x) + if iszero(eq(div(xx, x), x)) { revert(0, 0) } + let xxRound := add(xx, half) + if lt(xxRound, xx) { revert(0, 0) } + x := div(xxRound, RAY) + if mod(n, 2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { revert(0, 0) } + let zxRound := add(zx, half) + if lt(zxRound, zx) { revert(0, 0) } + z := div(zxRound, RAY) + } + } + } + } + } + + function _divup(uint256 x, uint256 y) internal pure returns (uint256 z) { + // Note: _divup(0,0) will return 0 differing from natural solidity division + unchecked { + z = x != 0 ? ((x - 1) / y) + 1 : 0; + } + } + + // --- Admin external functions --- + + function rely(address usr) external { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external { + wards[usr] = 0; + emit Deny(usr); + } + + function file(bytes32 what, uint256 data) external auth { + if (what == "ssr") { + require(data >= RAY, "SUsds/wrong-ssr-value"); + require(rho == block.timestamp, "SUsds/chi-not-up-to-date"); + ssr = data; + } else { + revert("SUsds/file-unrecognized-param"); + } + emit File(what, data); + } + + // --- Savings Rate Accumulation external/internal function --- + + function drip() public returns (uint256 nChi) { + (uint256 chi_, uint256 rho_) = (chi, rho); + uint256 diff = 0; + rho = block.timestamp; + emit Drip(nChi, diff); + } + + // --- ERC20 Mutations --- + + function transfer(address to, uint256 value) external returns (bool) { + require(to != address(0) && to != address(this), "SUsds/invalid-address"); + uint256 balance = balanceOf[msg.sender]; + require(balance >= value, "SUsds/insufficient-balance"); + + unchecked { + balanceOf[msg.sender] = balance - value; + balanceOf[to] += value; // note: we don't need an overflow check here b/c sum of all balances == totalSupply + } + + emit Transfer(msg.sender, to, value); + + return true; + } + + function transferFrom(address from, address to, uint256 value) external returns (bool) { + require(to != address(0) && to != address(this), "SUsds/invalid-address"); + uint256 balance = balanceOf[from]; + require(balance >= value, "SUsds/insufficient-balance"); + + if (from != msg.sender) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= value, "SUsds/insufficient-allowance"); + + unchecked { + allowance[from][msg.sender] = allowed - value; + } + } + } + + unchecked { + balanceOf[from] = balance - value; + balanceOf[to] += value; // note: we don't need an overflow check here b/c sum of all balances == totalSupply + } + + emit Transfer(from, to, value); + + return true; + } + + function approve(address spender, uint256 value) external returns (bool) { + allowance[msg.sender][spender] = value; + + emit Approval(msg.sender, spender, value); + + return true; + } + + // --- Mint/Burn Internal --- + + function _mint(uint256 assets, uint256 shares, address receiver) internal { + require(receiver != address(0) && receiver != address(this), "SUsds/invalid-address"); + + usds.transferFrom(msg.sender, address(this), assets); + + unchecked { + balanceOf[receiver] = balanceOf[receiver] + shares; // note: we don't need an overflow check here b/c balanceOf[receiver] <= totalSupply + totalSupply = totalSupply + shares; // note: we don't need an overflow check here b/c shares totalSupply will always be <= usds totalSupply + } + + emit Deposit(msg.sender, receiver, assets, shares); + emit Transfer(address(0), receiver, shares); + } + + function _burn(uint256 assets, uint256 shares, address receiver, address owner) internal { + uint256 balance = balanceOf[owner]; + require(balance >= shares, "SUsds/insufficient-balance"); + + if (owner != msg.sender) { + uint256 allowed = allowance[owner][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= shares, "SUsds/insufficient-allowance"); + + unchecked { + allowance[owner][msg.sender] = allowed - shares; + } + } + } + + unchecked { + balanceOf[owner] = balance - shares; // note: we don't need overflow checks b/c require(balance >= shares) and balance <= totalSupply + totalSupply = totalSupply - shares; + } + + usds.transfer(receiver, assets); + + emit Transfer(owner, address(0), shares); + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + // --- ERC-4626 --- + + function asset() external view returns (address) { + return address(usds); + } + + function totalAssets() external view returns (uint256) { + return convertToAssets(totalSupply); + } + + function convertToShares(uint256 assets) public view returns (uint256) { + uint256 chi_ = (block.timestamp > rho) ? _rpow(ssr, block.timestamp - rho) * chi / RAY : chi; + return assets * RAY / chi_; + } + + function convertToAssets(uint256 shares) public view returns (uint256) { + uint256 chi_ = (block.timestamp > rho) ? _rpow(ssr, block.timestamp - rho) * chi / RAY : chi; + return shares * chi_ / RAY; + } + + function maxDeposit(address) external pure returns (uint256) { + return type(uint256).max; + } + + function previewDeposit(uint256 assets) external view returns (uint256) { + return convertToShares(assets); + } + + function deposit(uint256 assets, address receiver) public returns (uint256 shares) { + shares = assets * RAY / drip(); + _mint(assets, shares, receiver); + } + + function deposit(uint256 assets, address receiver, uint16 referral) external returns (uint256 shares) { + shares = deposit(assets, receiver); + emit Referral(referral, receiver, assets, shares); + } + + function maxMint(address) external pure returns (uint256) { + return type(uint256).max; + } + + function previewMint(uint256 shares) external view returns (uint256) { + uint256 chi_ = (block.timestamp > rho) ? _rpow(ssr, block.timestamp - rho) * chi / RAY : chi; + return _divup(shares * chi_, RAY); + } + + function mint(uint256 shares, address receiver) public returns (uint256 assets) { + assets = _divup(shares * drip(), RAY); + _mint(assets, shares, receiver); + } + + function mint(uint256 shares, address receiver, uint16 referral) external returns (uint256 assets) { + assets = mint(shares, receiver); + emit Referral(referral, receiver, assets, shares); + } + + function maxWithdraw(address owner) external view returns (uint256) { + return convertToAssets(balanceOf[owner]); + } + + function previewWithdraw(uint256 assets) external view returns (uint256) { + uint256 chi_ = (block.timestamp > rho) ? _rpow(ssr, block.timestamp - rho) * chi / RAY : chi; + return _divup(assets * RAY, chi_); + } + + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) { + shares = _divup(assets * RAY, drip()); + _burn(assets, shares, receiver, owner); + } + + function maxRedeem(address owner) external view returns (uint256) { + return balanceOf[owner]; + } + + function previewRedeem(uint256 shares) external view returns (uint256) { + return convertToAssets(shares); + } + + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets) { + assets = shares * drip() / RAY; + _burn(assets, shares, receiver, owner); + } + + // --- Approve by signature --- + + function _isValidSignature(address signer, bytes32 digest, bytes memory signature) + internal + view + returns (bool valid) + { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + if (signer == ecrecover(digest, v, r, s)) { + return true; + } + } + + if (signer.code.length > 0) { + (bool success, bytes memory result) = + signer.staticcall(abi.encodeCall(IERC1271.isValidSignature, (digest, signature))); + valid = + (success && result.length == 32 && abi.decode(result, (bytes4)) == IERC1271.isValidSignature.selector); + } + } + + function permit(address owner, address spender, uint256 value, uint256 deadline, bytes memory signature) public { + require(block.timestamp <= deadline, "SUsds/permit-expired"); + require(owner != address(0), "SUsds/invalid-owner"); + + uint256 nonce; + unchecked { + nonce = nonces[owner]++; + } + + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + _calculateDomainSeparator(block.chainid), + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)) + ) + ); + + require(_isValidSignature(owner, digest, signature), "SUsds/invalid-permit"); + + allowance[owner][spender] = value; + emit Approval(owner, spender, value); + } + + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + { + permit(owner, spender, value, deadline, abi.encodePacked(r, s, v)); + } +} diff --git a/certora/mocks/Usds.sol b/certora/mocks/Usds.sol new file mode 100644 index 0000000..2a762b3 --- /dev/null +++ b/certora/mocks/Usds.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.21; + +contract Usds { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + uint256 public totalSupply; + + constructor(uint256 initialSupply) { + mint(msg.sender, initialSupply); + } + + function approve(address spender, uint256 value) external returns (bool) { + allowance[msg.sender][spender] = value; + return true; + } + + function transfer(address to, uint256 value) external returns (bool) { + uint256 balance = balanceOf[msg.sender]; + require(balance >= value, "Gem/insufficient-balance"); + + unchecked { + balanceOf[msg.sender] = balance - value; + balanceOf[to] += value; + } + return true; + } + + function transferFrom(address from, address to, uint256 value) external returns (bool) { + uint256 balance = balanceOf[from]; + require(balance >= value, "Gem/insufficient-balance"); + + if (from != msg.sender) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= value, "Gem/insufficient-allowance"); + + unchecked { + allowance[from][msg.sender] = allowed - value; + } + } + } + + unchecked { + balanceOf[from] = balance - value; + balanceOf[to] += value; + } + return true; + } + + function mint(address to, uint256 value) public { + unchecked { + balanceOf[to] = balanceOf[to] + value; + } + totalSupply = totalSupply + value; + } + + function burn(address from, uint256 value) external { + uint256 balance = balanceOf[from]; + require(balance >= value, "Gem/insufficient-balance"); + + if (from != msg.sender) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= value, "Gem/insufficient-allowance"); + + unchecked { + allowance[from][msg.sender] = allowed - value; + } + } + } + + unchecked { + balanceOf[from] = balance - value; + totalSupply = totalSupply - value; + } + } +} diff --git a/certora/mocks/UsdsJoin.sol b/certora/mocks/UsdsJoin.sol new file mode 100644 index 0000000..39fa848 --- /dev/null +++ b/certora/mocks/UsdsJoin.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.21; + +interface UsdsLike { + function burn(address, uint256) external; + function mint(address, uint256) external; +} + +interface VatLike { + function move(address, address, uint256) external; +} + +contract UsdsJoin { + VatLike public immutable vat; // CDP Engine + UsdsLike public immutable usds; // Stablecoin Token + + uint256 constant RAY = 10 ** 27; + + // --- Events --- + event Join(address indexed caller, address indexed usr, uint256 wad); + event Exit(address indexed caller, address indexed usr, uint256 wad); + + constructor(address vat_, address usds_) { + vat = VatLike(vat_); + usds = UsdsLike(usds_); + } + + function join(address usr, uint256 wad) external { + vat.move(address(this), usr, RAY * wad); + usds.burn(msg.sender, wad); + emit Join(msg.sender, usr, wad); + } + + function exit(address usr, uint256 wad) external { + vat.move(msg.sender, address(this), RAY * wad); + usds.mint(usr, wad); + emit Exit(msg.sender, usr, wad); + } + + // To fully cover daiJoin abi + function dai() external view returns (address) { + return address(usds); + } +} diff --git a/certora/mocks/Vat.sol b/certora/mocks/Vat.sol new file mode 100644 index 0000000..858fdf2 --- /dev/null +++ b/certora/mocks/Vat.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/// vat.sol -- Dai CDP database + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.5.12; + +contract Vat { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address usr) external note auth { + wards[usr] = 1; + } + + function deny(address usr) external note auth { + wards[usr] = 0; + } + + modifier auth() { + require(wards[msg.sender] == 1, "Vat/not-authorized"); + _; + } + + mapping(address => mapping(address => uint256)) public can; + + function hope(address usr) external note { + can[msg.sender][usr] = 1; + } + + function nope(address usr) external note { + can[msg.sender][usr] = 0; + } + + function wish(address bit, address usr) internal view returns (bool) { + return either(bit == usr, can[bit][usr] == 1); + } + + // --- Data --- + struct Ilk { + uint256 Art; // Total Normalised Debt [wad] + uint256 rate; // Accumulated Rates [ray] + uint256 spot; // Price with Safety Margin [ray] + uint256 line; // Debt Ceiling [rad] + uint256 dust; // Urn Debt Floor [rad] + } + + struct Urn { + uint256 ink; // Locked Collateral [wad] + uint256 art; // Normalised Debt [wad] + } + + mapping(bytes32 => Ilk) public ilks; + mapping(bytes32 => mapping(address => Urn)) public urns; + mapping(bytes32 => mapping(address => uint256)) public gem; // [wad] + mapping(address => uint256) public dai; // [rad] + mapping(address => uint256) public sin; // [rad] + + uint256 public debt; // Total Dai Issued [rad] + uint256 public vice; // Total Unbacked Dai [rad] + uint256 public Line; // Total Debt Ceiling [rad] + uint256 public live; // Active Flag + + // --- Logs --- + event LogNote( + bytes4 indexed sig, bytes32 indexed arg1, bytes32 indexed arg2, bytes32 indexed arg3, bytes data + ) anonymous; + + modifier note() { + _; + assembly { + // log an 'anonymous' event with a constant 6 words of calldata + // and four indexed topics: the selector and the first three args + let mark := msize() // end of memory ensures zero + mstore(0x40, add(mark, 288)) // update free memory pointer + mstore(mark, 0x20) // bytes type data offset + mstore(add(mark, 0x20), 224) // bytes size (padded) + calldatacopy(add(mark, 0x40), 0, 224) // bytes payload + log4( + mark, + 288, // calldata + shl(224, shr(224, calldataload(0))), // msg.sig + calldataload(4), // arg1 + calldataload(36), // arg2 + calldataload(68) // arg3 + ) + } + } + + // --- Init --- + constructor() public { + wards[msg.sender] = 1; + live = 1; + } + + // --- Math --- + function add(uint256 x, int256 y) internal pure returns (uint256 z) { + z = x + uint256(y); + require(y >= 0 || z <= x); + require(y <= 0 || z >= x); + } + + function sub(uint256 x, int256 y) internal pure returns (uint256 z) { + z = x - uint256(y); + require(y <= 0 || z <= x); + require(y >= 0 || z >= x); + } + + function mul(uint256 x, int256 y) internal pure returns (int256 z) { + z = int256(x) * y; + require(int256(x) >= 0); + require(y == 0 || z / y == int256(x)); + } + + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + // --- Administration --- + function init(bytes32 ilk) external note { + require(ilks[ilk].rate == 0, "Vat/ilk-already-init"); + ilks[ilk].rate = 10 ** 27; + } + + function file(bytes32 what, uint256 data) external note { + if (what == "Line") Line = data; + else revert("Vat/file-unrecognized-param"); + } + + function file(bytes32 ilk, bytes32 what, uint256 data) external note { + if (what == "spot") ilks[ilk].spot = data; + else if (what == "line") ilks[ilk].line = data; + else if (what == "dust") ilks[ilk].dust = data; + else revert("Vat/file-unrecognized-param"); + } + + function cage() external note { + live = 0; + } + + // --- Fungibility --- + function slip(bytes32 ilk, address usr, int256 wad) external note { + gem[ilk][usr] = add(gem[ilk][usr], wad); + } + + function flux(bytes32 ilk, address src, address dst, uint256 wad) external note { + require(wish(src, msg.sender), "Vat/not-allowed"); + gem[ilk][src] = sub(gem[ilk][src], wad); + gem[ilk][dst] = add(gem[ilk][dst], wad); + } + + function move(address src, address dst, uint256 rad) external note { + require(wish(src, msg.sender), "Vat/not-allowed"); + dai[src] = sub(dai[src], rad); + dai[dst] = add(dai[dst], rad); + } + + function either(bool x, bool y) internal pure returns (bool z) { + assembly { + z := or(x, y) + } + } + + function both(bool x, bool y) internal pure returns (bool z) { + assembly { + z := and(x, y) + } + } + + // --- CDP Manipulation --- + function frob(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external note { + Urn memory urn = urns[i][u]; + Ilk memory ilk = ilks[i]; + // ilk has been initialised + require(ilk.rate != 0, "Vat/ilk-not-init"); + + urn.ink = add(urn.ink, dink); + urn.art = add(urn.art, dart); + ilk.Art = add(ilk.Art, dart); + + int256 dtab = mul(ilk.rate, dart); + uint256 tab = mul(ilk.rate, urn.art); + debt = add(debt, dtab); + + // either debt has decreased, or debt ceilings are not exceeded + require(either(dart <= 0, both(mul(ilk.Art, ilk.rate) <= ilk.line, debt <= Line)), "Vat/ceiling-exceeded"); + // urn is either less risky than before, or it is safe + require(either(both(dart <= 0, dink >= 0), tab <= mul(urn.ink, ilk.spot)), "Vat/not-safe"); + + // urn is either more safe, or the owner consents + require(either(both(dart <= 0, dink >= 0), wish(u, msg.sender)), "Vat/not-allowed-u"); + // collateral src consents + require(either(dink <= 0, wish(v, msg.sender)), "Vat/not-allowed-v"); + // debt dst consents + require(either(dart >= 0, wish(w, msg.sender)), "Vat/not-allowed-w"); + + // urn has no debt, or a non-dusty amount + require(either(urn.art == 0, tab >= ilk.dust), "Vat/dust"); + + gem[i][v] = sub(gem[i][v], dink); + dai[w] = add(dai[w], dtab); + + urns[i][u] = urn; + ilks[i] = ilk; + } + // --- CDP Fungibility --- + + function fork(bytes32 ilk, address src, address dst, int256 dink, int256 dart) external note { + Urn storage u = urns[ilk][src]; + Urn storage v = urns[ilk][dst]; + Ilk storage i = ilks[ilk]; + + u.ink = sub(u.ink, dink); + u.art = sub(u.art, dart); + v.ink = add(v.ink, dink); + v.art = add(v.art, dart); + + uint256 utab = mul(u.art, i.rate); + uint256 vtab = mul(v.art, i.rate); + + // both sides consent + require(both(wish(src, msg.sender), wish(dst, msg.sender)), "Vat/not-allowed"); + + // both sides safe + require(utab <= mul(u.ink, i.spot), "Vat/not-safe-src"); + require(vtab <= mul(v.ink, i.spot), "Vat/not-safe-dst"); + + // both sides non-dusty + require(either(utab >= i.dust, u.art == 0), "Vat/dust-src"); + require(either(vtab >= i.dust, v.art == 0), "Vat/dust-dst"); + } + // --- CDP Confiscation --- + + function grab(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external note { + Urn storage urn = urns[i][u]; + Ilk storage ilk = ilks[i]; + + urn.ink = add(urn.ink, dink); + urn.art = add(urn.art, dart); + ilk.Art = add(ilk.Art, dart); + + int256 dtab = mul(ilk.rate, dart); + + gem[i][v] = sub(gem[i][v], dink); + sin[w] = sub(sin[w], dtab); + vice = sub(vice, dtab); + } + + // --- Settlement --- + function heal(uint256 rad) external note { + address u = msg.sender; + sin[u] = sub(sin[u], rad); + dai[u] = sub(dai[u], rad); + vice = sub(vice, rad); + debt = sub(debt, rad); + } + + function suck(address u, address v, uint256 rad) external note { + sin[u] = add(sin[u], rad); + dai[v] = add(dai[v], rad); + vice = add(vice, rad); + debt = add(debt, rad); + } + + // --- Rates --- + function fold(bytes32 i, address u, int256 rate) external note { + Ilk storage ilk = ilks[i]; + ilk.rate = add(ilk.rate, rate); + int256 rad = mul(ilk.Art, rate); + dai[u] = add(dai[u], rad); + debt = add(debt, rad); + } +} diff --git a/src/SPBEAM.t.sol b/src/SPBEAM.t.sol index 318b1af..49794e5 100644 --- a/src/SPBEAM.t.sol +++ b/src/SPBEAM.t.sol @@ -43,8 +43,12 @@ contract InitCaller { } } -contract MockBrokenConv is ConvMock { - function btor(uint256 /* bps */ ) public pure override returns (uint256) { +contract MockBrokenConv { + function rtob(uint256 /* ray */ ) public pure returns (uint256) { + return 0; + } + + function btor(uint256 /* bps */ ) public pure returns (uint256) { return 0; } } diff --git a/src/mocks/ConvMock.sol b/src/mocks/ConvMock.sol index b2ae523..5c1df40 100644 --- a/src/mocks/ConvMock.sol +++ b/src/mocks/ConvMock.sol @@ -1,32 +1,12 @@ // SPDX-FileCopyrightText: © 2025 Dai Foundation // SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - pragma solidity ^0.8.24; contract ConvMock { - /// @notice The max bps supported in bps -> rate conversion. - uint256 public constant MAX = 50_00; - /// @dev `ray` precision + uint256 public constant MAX_BPS_IN = 50_00; uint256 internal constant RAY = 10 ** 27; - /// @dev `bps` precision uint256 internal constant BPS = 100_00; - // Each rate takes 8 bytes (64 bits), total of 5001 rates - // Each storage word (32 bytes) contains exactly 4 rates - // Total size = 5001 * 8 = 40008 bytes bytes internal RATES; constructor() { @@ -34,12 +14,8 @@ contract ConvMock { hex"0000000000000000000b43d83715d0b0001687669d710caf0021caab36d8f947002d0da6071491820038505711ea862d004392be5b213de3004ed4dbe67ed513005a16afb7c91e0700655839d2c5a0eb0070997a3b399bdb007bda70f4ea02de00871b1e039b7ffc00925b816b12733a009d9b9b2f12f2a800a8db6b5360ca6800b41af1dbbf7cb100bf5a2ecbf241dd00ca992227bc086b00d5d7cbf2df750a00e1162c311ee2a100ec5442e63c625200f7921015f9bb870102cf93c4186bf7010e0ccdf459a7aa011949beaa7e590a01248665ea4720e1012fc2c3b7745664013afed815c6073d01463aa308fbf78e0151762494d5a1fb015cb15cbd1237b30167ec4b8570a075017326f0f1af7a96017e614d058d1b0f01899b5fc4c78d7b0194d529331c942801a00ea95449a81901ab47e02c0bf90d01b680cdbe206d8a01c1b9720e43a2e201ccf1cd2031ed3b01d829def7a7579701e361a7985fa3d901ee992706164ad201f9d05d44867c420205074a576b1ee202103dee427ed06f021b7449097be5aa0226aa5ab01c6a680231e0233a1a2191023d15a2ab2e852e02484ad90712c66e02537fc6517fcdac025eb46a8e2e3a790269e8c5c0d663a102751cd7ed305735028050a116f3da8e028b842141d86a5d0296b75871953aa702a1ea46a9e136d602ad1cebee7301ba02b84f484300f59502c3815bab41242002ceb3262ae9569102d9e4a7c5af0da602e515e07f4781ab02f046d05b67a27f02fb77775dc4179d0306a7d58a1140260311d7eae40332e4031d07b76f4dbe540328373b2fa468ac0333667628ba6fe4033e95685e42c9bc0349c411d3f023c40354f2728d74e3620360208a8e8325db036b4e59daccc05903767be076033ff40381a91e63d7e9b9038cd613a7fbbaae039802c0461f67de03a32f2441f35e5f03ae5b3f9f27c35703b98712616c740603c4b29c8c7105ca03cfddde23e4c62903db08d72b76badd03e63387a6d5a1ce03f15def99aff12703fc880f07b3d7560407b1e5f48f3b140412db7463efbb6e041e04ba5982afcc04292db7d8f527f80434566ce5f3ec23043f7ed9842b7cef044aa6fdb74813770455ced982f5a1540460f66ceadfd0a5046c1db7f2b20414047744ba9e1756e204826b74f0ba9cee048d91e6ee4662b50498b8109a64ed6004a3ddf1f8c03acb04af038b0d02018a04ba28dbdad3b0f004c54de465de711704d072a4b1cb22e804db971cc242602204e6bb4c9aec7b6004f1df343f71802204fd02d3b37932d10508262afaab10ca0513493a18ae5063051e6c011129e0f305298e7fe7c46ada0534b0b6a0244f83053fd2a53defa975054af44bc4cc4c50055615aa385fc4db056136c09c4f5907056c578ef44007fa0577781543d68a12058298538eb750f1058db849d886877d0598d7f824e811f005a3f75e777f8dd905af167cd3f0522505ba35533ddd6f2605c553e1b8e9ae9805d0722848b793ae05db9026f0e95b1405e6adddb520faf605f1cb4c9900230b05fce873a0283c9606080552ce3a6a77061321ea26d78926061e3e39ada02ec406295a416634ab1c06347601543507ad063f91797b4107b0064aaca9def828210655c79282f99fc20660e2336ae45f29066bfc8c9a5710bc0677169e14f018c606823067de4d9573068d49e9fa0d5eda069863246bcd070906a37c173729da0406ae94c25fc0ddd406b9ad25e92ed28606c4c541d710323b06cfdd162d01312506daf4a2ee9dbd9a06e60be81f81800e06f122e5c347db2306fc399bdd8bebb00707500a71e888c10712663183f843a8071d7c11175567f8072891a92f99fb990733a6f9d05fbec4073ebc02fd402c110749d0c4b9d4787c0754e53f09b5936a075ff971f07c26b4076b0d5d71c096a907762101911b02190781345e5223425c078c4773b870eb5607975a41c79b4b8007a26cc883396bf007ad7f07eee2105b07b891000e2bb72507c3a2b0e4ac995e07ceb41a75faaad107d9c53cc5ab9a0407e4d617d754d04707efe6abae8b71b207faf6f84ee45d33080606fdbbf42c93081116bbf94f347b081c26330a89847a08273562f336e7150832444bb6eae1bf083d52ed5938b4ef08486147ddb35c1d08536f5b47ed8dca085e7d279b79bb8e08698aacdbea1215087497eb0cd0792f087fa4e231be93ce088ab1924e45c0150895bdfb65f7175c08a0ca1d7c636e3408abd5f8951b547108b6e18cb3af153208c1ecd9dbaeb6e408ccf7e010a9fb4b08d8029f56305f8c08e30d17afd11c2c08ee1749211b252008f92133ad9d29ce09042ad758e59515090f343426828d54091a3d4a1a01f4720925461936f167e409304ea180de40b5093b56e2fb55938809465edda9e430a8095166919016a406095c6dfeb1793544096775251197e7bb09727c04b3fe7a83097d829d9c386877098888efcdd0e84109938efb4c52ec5b099e94c01b49231809a99a3e3e3df6ab09b49f75b8bb8d3409bfa4668e4bc8b709caa910c278473409d5ad7458ca62a509e0b19154cb310409ebb567ba03845709f6b8f78bfbeab20a01bc40ce3cae3f0a0cbf43844dd5490a17c1ffb1b7223d0a22c4755a0013b10a2dc6a480afe4720a38c88d294d8b810a43ca2f575fbc210a4ecb8b0e6ce5da0a59cca051fb34810a64cd6f259090410a6fcdf78cb29d9c0a7ace398ae6bd780a85ce3523b20d230a90cdea5a9966570a9bcd5933215f460aa6cc81b0ce4aa00ab1cb63d72437940abcc9ffa9a6f1de0ac7c8552bda01c80ad2c6646140ac360addc42d4d5df2a70ae8c1aff3b493420af3beec57c708d90afebbe27d178aec0b09b89267280dba0b14b4fc197a423e0b1fb11f978f96390b2aacfce4e9343d0b35a894050803aa0b40a3e4fb6ca8bf0b4b9eefcb97849c0b5699b47908b5450b619433074015b10b6c8e6b79bd3dca0b77885dd3ff82780b82820a1985f5a20b8d7b704dcf663b0b987490745a60460ba36d6a90a52cdd0bae65fea62dd2370bb95e4cb87213ad0bc45654caef71c50bcf4e16e1232a360bda4592fe8a37ef0be53cc926a1531d0bf033b95ce4f1320bfb2a63a4d144ef0c0620c801e23e650c1116e677938afd0c1c0cbf096095830c270251bac486290c31f79e8f3a428c0c3ceca58a3c6dbe0c47e166af45684c0c52d5e201cf50440c5dca178554013d0c68be073d4d14580c73b1b12d33e04f0c7ea515588179770c899833c2aeb1c60c948b0c6f3418da0c9f7d9f6189fc030caa6fec9d2866420cb561f42587205d0cc053b5fe1db0d50ccb45322a635bf90cd63668adcf23e60ce127598bd7c8920cec1804c7f3c7cf0cf7086a65995d540d01f88a683e82c10d0ce864d358efab0d17d7f9aa5e19990d22c748f0c334150d2db652a9fd30ad0d38a516d980bef80d43939582c24ca20d4e81cea936056e0d596fc2504fd33e0d645d707b835e1b0d6f4ad92e440c380d7a37fc6c0501fc0d8524da383922080d90117296530d3b0d9afdc589c522bc0da5e9d316017ffe0db0d59b3e7a00c80dbbc11e06a03f380dc6ac5b71e593d00dd1975383bb15790ddc82063f9199860de76c73a8d9b3c00df2569bc303b66a0dfd407e917fb24a0e082a1c17bd76ab0e131374592c91650e1dfc87593c4ee70e28e5551b5bba3b0e33cddda2f99d0a0e3eb620f3847fa50e499e1f106aa90f0e5485d7fd1a1efa0e5f6d4bbd00a5d90e6a547a538bc0dd0e753b63c428b2010e80220812447a0d0e8b0867414bd89f0e95ee8154ab4c330ea0d4564fcf12230eabb9e6362326b40eb69f310b1345190ec18436d20ae77a0ecc68f78e7546fc0ed74d7343bd5bc60ee231a9f54ddd060eed159ba69140fc0ef7f9485af1bcfc0f02dcb015d945760f0dbfd2dab18dfb0f18a2b0ace409470f2385498fd9e9440f2e679d86fc1f140f3949ac95b35b110f442b76bf680cda0f4f0cfc078263570f59ee3c716a4cbf0f64cf380087769d0f6fafeeb8414ddb0f7a90609bfefec20f85708daf2775080f905075f5215bce0f9b301971531dad0fa60f782722e4b90fb0ee9219f69a8b0fbbcd674d33e83f0fc6abf7c44036860fd18a438280ada40fdc684a8b5a35770fe7460ce23175840ff2238a8a6ad4f30ffd00c3876a7a9f1007ddb7dc944d161012ba678d4bf2a3101d96d29cf4d151102872f90ef20ef510334edae6a69133103e2a782774fd82104905d0d4bfb9361053e0e4f1e8e984105ebbb4825273881069963f895dfc4f107470860a6ce8d6107f4a8808e05e19108a2445881941151094fdbe8b7836cc109fd6f3165da45010aaafe32c29aec410b5888ed03c3b6b10c060f605f4efa410cb3918d0b330f610d610f733d6251610e0e89132bcb1ed10ebbfe6d0c57d9e10f696f8114eee8a11016dc4f7b72b5c110c444d875c1b0711171a91c39b64d51121f091afd2706a112cc64d4f5e65c411379bc4a59c2d4d114270f7b5e86fd8114d45e6839f96ab11581a91121dcb841162eef764bef8a0116dc3197edec8c3117896f763d8a73911836a911707bfe2118e3de69bc6ff36119910f7f571124911a3e3c5276066d511aeb64e34ef2b3f11b9889321774e9b11c45a93f05280b611cf2c50a4da321811d9fdc94267940e11e4cefdcc5398af11ef9fee45f6f2e011fa709ab2aa165e1205410315c537c11210112772a04c86121ae107cc930b0f1225b0a426f4eaad12307ffc851d23ac123b4f10ea62af4c12461de15a1c47d21250ec6dd7a0688b125bbab666454dd1126688bb0960f5131271567bc4491cd8127c23f89a5344c81286f1318ed4adb51291be26a5225999129c8ad7e0910ba512a757454475483e12b2236ed423550f12bcef5492ef390212c7baf6842cbc5112d28654ab2f688512dd516f0b4a888112e81c45a7d1288312f2e6d88416163012fdb127a36be09713087b330924d836131344fab8930f02131e0e7eb508586d1328d7bf01d6496c1333a0bba24e387d133e697499c13dac134931e9eb80329b1353fa1b9adbb289135ec209ab241a53136989b41fa9887e1374511afbbbdd41137f183e42aaba821389df1df7c583e11394a5ba1e5b5ec2139f6c12b9bb324b13aa3227cd33a77013b4f7f95c1328f613bfbd8769a7e37b13ca82d1f93fc57d13d547d90e287f5c13e00c9cabaf836413ead11cd52205cf13f595598dccfcd314005952d8fd209e140b1d08b9feeb621415e07b341e995c1420a3aa4aa828d6142b669600e75a2e1436293e5a27afe11440eba359b46e88144badc502d89ce714566fa358df03ef1461313e5f122ec2146bf29618bc6abd1476b3aa8927c77d1481747bb39e16e2148c35099b68ed1b1496f55443d1a0a414a1b55bb0214a5514ac751fe3a0c55e14b734a0e198af5b14c1f3dead51684814ccb2d94a13129714d77190bb25932e14e2300503d0916d14ecee36275b773714f7ac24290d70f9150269cf0c2d6da7150d2736d4021ecf1517e45b83d1f8971522a13d1ee331c1152d5ddba87bc3ba15381a3723e16a971542d64f9459a51f154d9224fd29b4d215584db761969ded15630906c4e5276f156dc4132a59db2215787edc953905a01583396308c6b656158df3a68846bf8e1598ada716fcb67315a36764b82bf31815ae20df6f17907c15b8da173f026c9215c3930c2b2f284715ce4bbe36e0278515d9042d6557913b15e3bc59b9d74f6715ee744337a10f1215f92be9e1f640601603e34dbc181691160e9a6ec94788081619514d0cc54e4e162407e889d1e61f162ebe4143ad8f66163974573d984d4d16442a2a7ad1e63f164edfbafe99e3e716599508cc2f934316644a13e6d204a0166efedc51c00ba11679b36210383f4b168467a52578fa05168f1ba594c0599f1699cf63614c3f5816a482de8e5a4fe816af36171f27f37d16b9e90d16f255cb16c49bc078f6660916cf4e314870d70016da005f889e1f0716e4b24b3cba781116ef63f46801dfad16fa155b0db017111704c67f3100a31b170f7760d52ecc5a171a27fffd759f161724d85cad0feb4e172f8876e73844c6173a384eaf2903081744e7e4081c416c174f9736f54bdf1f175a464779f17f261764f51599468862176fa3a15684259e177a51eab4e3458c1784fff1b79c9ad0178fadb661e89c05179a5b38b6ff83c017a50878ba19509a17afb5766e6dc53317ba6231d734683817c50eaaf7a4846c17cfbae1d2f528a717da66d66c5d27e317e51288c713194017efbdf8e64d580617fa6926cd4203af180514127f26ffea180fbebbff31f4a4181a692350984e0a18251348768f3c92182fbd2b744bb4fe183a66cc4d0270671845102b03e7ec38184fb9479c306a43185a6222190ff0b818650aba7dba4a35186fb310cd6305c7187a5b250b3d76f1188502f73a7cb5b2188faa875e539e8a189a51d579f4d28118a4f8e19092b72918af9faba55f76ab18ba4633bb8cffc718c4ec79d64d05d918cf927df8d100e318da3840264a2d9218e4ddc061e98d3e18ef82feaedfe5f818fa27fb105dc28d1904ccb589937287190f712e1db10a37191a1564cfe662bc1924b959a3631a07192f5d0c9b5692de193a007dbaeff4e61944a3ad055e2ca7194f469a7dcfeb921959e9462773a80419648bb005779d53196f2dd81b09cbca1979cfbe6b57f8b619847162f98fae69198f12c5c8de3c3f1999b3e6dc70b6a719a454c63773f72219aef563dd149c5219b995bfd07f09f819c435da14df68fc19ced5b2ad61a77519d975499d3178ab19e4149ee77a552119eeb3b28f677a9319f952849823ec071a03f11504da71c91a0e8f63d8b599731a192d7116dfb5f71a23cb3cc282dfa11a2e68c6dec8f41a1a39060f6edb96741a43a31675e42f2c1a4e3fdbf70bec2e1a58dc5ff57bc0de1a6378a2745c661e1a6e14a376d65a4f1a78b0630011e15c1a834be1133704bd1a8de71db36d937a1a988218e3dd223a1aa31cd2a7ad0b391aadb74b02046e611ab85181f60a313d1ac2eb7786e4ff0c1acd852bb7bb48bf1ad81e9e8bb345031ae2b7d005f2f0451aed50c029a00cb61af7e96ef9e022551b0281dc79d87eec1b0d1a08acae36221b17b1f3958621771b22499d3784e04a1b2ce10595ced7e71b37782cb38833811b420f1293d4e4411b4ca5b739d8a1461b573c1aa8b6e7af1b61d23ce392fa991b6c681ded8fe32f1b76fdbdc9d070a71b81931c7b77384d1b8c283a05a695851b96bd166b80a9d21ba151b1b0275cdb1babe60bd6bc5c741bb67a24e2611c9b1bc10dfcd636d78a1bcba193b55e8daf1bd634e982f905bf1be0c7fe4226ccaf1beb5ad1f60835c41bf5ed64a1bd5a901c007fb648661aff1c0b11c6ed221d561c15a3969310ce3d1c2035253d5160c41c2ac672ef02ce671c35577fab43d7121c3fe84b7533012c1c4a78d64fee99981c5509203e94b3ba1c5f9929444329801c6a28f164179b671c74b878a12f707d1c7f47befea7d6691c89d6c47f9dc1721c946589272dec811c9ef40cf874d9291ca9824ff68ecfad1cb410522497df021cbe9e1385abdcd81cc92b941ce665a01cd3b8d3ed62dc8d1cde45d2fa3c6b9c1ce8d291468e039d1cf35f0ed5725c341cfdeb4baa03f3dd1d087747c75d0ff91d1303033097bcca1d1d8e7de8cdcd811d2819b7f318dc3d1d32a4b152924a141d3d2f6a0a533f181d47b9e21d74aa5b1d5244198f0f41f81d5cce10623b83121d6757c69a11b1e01d71e13c39a9d9b11d7c6a71441bccec1d86f365bc7f251f1d917c19a5eb42fb1d9c048d03774e601da68cbfd83a36601db114b2274ab1441dbb9c63f3bf3c8f1dc623d540ae1d0e1dd0ab06112d5ecc1ddb31f66852d52b1de5b8a649341ad81df03f15b6e691de1dfac544b47f639f1e054b33451380e61e0fd0e16bb7a1e51e1a564f2b8046381e24db7c8781b4f11e2f606982cffc9d1e39e516207ef3431e44698263a2366f1e4eedae4f4d2b341e597199e692fe371e63f5452c86a3af1e6e78b0243ad7691e78fbdad0c21cd31e837ec5352ebf001e8e016f5492d0a91e9883d932002c391ea30602d08873cb1ead87ec333d11361eb809955d2f360f1ec28afe516fdbaf1ecd0c27130fc3381ed78d0fa51f759c1ee20db80aaf439e1eec8e2046cf45df1ef70e485c8f5cdb1f018e304eff30f41f0c0dd8212e32751f168d3fd62b99991f210c677106668e1f2b8b4ef4cd617b1f3609f6648f1a891f40885dc359e9e01f4b0685143befb71f55846c5a4314521f600213987d08081f6a7f7ad1f7434b1f74fca209bf06ab1f7f798942e15adf1f89f630806b10c31f947297c568c1651f9eeebf14e6ce081fa96aa671f160261fb3e64ddf94697a1fbe61b560dba4011fc8dcdcf8d292041fd357c4aa847e1a1fddd26c78fc7b2d1fe84cd4674564801ff2c6fc7869ddb81ffd40e4af7452db2007ba8d0f6ef858201233f59b63cb0f201cad1e565c9053202726074362d5ef20319eb0657ff22f203c1719bfbd03de20468f435522f2562051072d28ba6d7b205b7ed73d8bedc52065f641969fb44920706d6c36fdcab3207ae45721ae035920855b0259b7f937208fd16de2230ff6209a4799bdf673f220a4bd85f0391a4320af33327bf1c0ba20b9a89f6426edee20c41dccabdef13e20ce92ba561fe2d520d9076865efa3b220e37bd6de53ddaa20edf005c252037320f863f514ef50a02102d7a4d930c9b1210d4b15121b3c102117be45c2b33e1a21223136edfd2f20212ca3e896fd37772137165ac0b7486f2141888d6e2f1c61214bfa80a26836b421566c346065e3df2160dda8ab2b396d216b4edd85bb160b2175bfd2f318218221803088f644ccc3218aa0ff924351eb21951136ca15b447219f812ea0bdc05921a9f0e7193d0be221b460603694f5dd21becf99fbc6a69021c93e946bd30f8a21d3ad4f89baeba821de1bcb587ebf1e21e88a07db1ed77b21f2f805149b4bab21fd65c307f3fbff2207d341b82892342212408128388175221cad815b23065f22271a4253e7270b223186c41583b30e223bf306a2f7438422465f09ff403b0e2250cace2d5cc5de225b3653304ad9b52265a1990b0835f322700c9fc092638e227a776753e6b5252284e1efc80246f9228f4c391fe1fefb2299b6435e828ccf22a4200e86e069cc22ae899a9bf7d90a22b8f2e7a0c4e75f22c35bf598436b6a22cdc4c4856f059322d82d546b43201422e295a54cbaeeff22ecfdb72cd1703e22f7658a0e816b9b2301cd1df4c572c9230c3472e297e16223169b88daf2dcee2321025fe0d054f1232b68f7f72a02df2335cf5120f96a332340356b6137d866234a9b46bade64ff235500e330e5f18e235f6640c64729bd2369cb5f7dfa83462374303f5af83e06237e94e0603863fc2388f94290b2c94f23935d65ef5f0c50239dc14a7f34958323a824f0432a97a323b288573e380fa823bceb7f7353c4cb23c74e68e574488823d1b113978ff6a923dc137f8c9cf54823e675acc79134d423f0d79b4b62701623fb394b1b062c3724059abc3971b8c4240ffbeea99a2fb3241a5ce26e7475692424bd978af538c0242f1e0e0210f30924397e45d6bbe8132443de3f0bea2631244e3df9a48f863c24589d75a39fab9c2462fcb30c0e0448246d5bb1e0cdc8d12477ba7224d1fc62248218f3db0d6cc8248c77370672b2772496d53ba9f4308a24a13301c88414d124ab9089651457ce24b5edd28296bcbf24c04add23fcd1a024caa7a94c37ef3224d50436fe3938fd24df60863cf19d5b24e9bc970b51d57524f418696c4a655124fe73fd62cb9bcd2508cf52f1c592ad25132a6a1c282e9c251d8542e4e31f2e2527dfdd4ee5deec25323a395d1fb353253c9457127facdc2546ee3671f4a700255147d77e6d483e255ba13a3ad8021d2565fa5eaa23113625705344cf3c7d34257aabecad1218dc258504564691820f258f5c819ea821d52599b46eb8432c5c25a40c1d964fa0fd25ae638e3bba4a4825b8bac0ab6fbdfd25c311b4e85c5d1e25cd686af56c53ea25d7bee2d58b99e925e2151c8ba5f1ea25ec6b181aa6ea0f25f6c0d58579dbcd26011654cf09ebf2260b6b95fa420aad2615c0990a0cf38d2620155e01552d8d262a69e4e3050b142634be2db206a9f8263f12387143f38d2649660523a69c9d2653b993cc182577265e0ce46d81d9ec26685ff70accd15c2672b2cba6e1eeb6267d056244a9e07c268757bae70d20cb2691a9d590f3f55e269bfbb245466f9526a64d5106ec6c7626b09eb1d8cd94b626baefd4bdd15cbd26c540b9b8df04a726cf9160ccdd985026d9e1c9fcb3ef5326e431f54b48ad1026ee81e2bb8240b326f8d1925046e538270321040c7ca16e270d7037f30948002717bf2e06d2777427220de64abd9a36272c5c60c1afe6982736aa9d6e8e5eda2740f89c543dd12f274b465d75a2d7bd275593e0d5a1d8ac275fe126771f061c276a2e2e5cfe5e3a27747af88a23ab39277ec7850172835c278913d3c5ce48fa27935fe4da1a2a82279dabb84139228427a7f74dfe0df7ad27b242a6137b3cd727bc8dc08463510227c6d89d53a85f6627d1233c842c5f6b27db6d9e18d114b727e5b7c214780f2f27f001a87a02aafc27fa4b514c521091280494bc8e4734ac280eddea42c2d863281926da6ca5892128236f8d0ecfa0ac282db8022c21452e28380039c77a693828424833e3bacbc2284c8ff083c1f8372856d76faa6f467728611eb15aa1dada286b65b59738a6362875ac7c631265e5287ff305c10da3cb288a3951b408b65428947f603ee1c082289ec5316476b1ec28a90ac527a546c228b3501b8b4b07d728bd953492454aa128c7da103f71313f28d21eae95abaa8128dc630f97d171e828e6a73348bf0fad28f0eb19ab50d8c528fb2ec2c262eeea2905722e90d14099290fb55d1977891a2919f84e5f31508629243b0264d9ebca292e7d792d4c7cad2938bfb2bb63f1d5294301af11fb06c7294d436e33ec43f4295784f02411feb82961c634e546595f296c073c7a63432c29764806e642785e298088942bbd8230298ac8e44dadb6e4299508f74eec39c5299f48cd3251fb2b29a98865fab7b87f29b3c7c1aaf5fc4329be06e045e51e1529c845c1ce5d42b329d2846647365c0229dcc2cdb348290c29e700f8156a361229f13ee57073dc8229fb7c95c73c43052a05ba091c9a5d802a0ff73f7364ed1c2a1a3438ce7280462a2470f5309972b82a2ead749cafed7a2a38e9b7158be6ec2a4325bc9e0322c22a4d618538eb32142a579d10e91973592a61d85fb16312712a6c1371949d08a82a764e46959c1cbb2a8088deb734e2dc2a8ac339fc3bbcb82a94fd586784d97b2a9f3739fbe435d42aa970debc2d9bfb2ab3aa46ab34a3b52abde371cbccb25a2ac81c6020c8fad52ad25511acfc7daf2adc8d86733a09122ae6c5be765438c92af0fdb9b91d764b2afb35783e67f8bb2b056cfa0905c4ef2b0fa43f1bc8ad752b19db47798252972b2412132504225e2b2e48a2211f589b2b387ef470a4fee42b42b50a1665eca42b4ceae31532c7132b57207f6fdc01462b6155df2931dc292b6b8b02440466902b75bfe8c3237d2f2b7ff492a95ecaa62b8a28fff985c7852b945d30b667ba512b9e9124e2d3b7842ba8c4dc8198a1982bb2f857958529062bbd2b962167cc512bc75e98280ed8022bd1915dac4866b62bdbc3e6b0e2611b2be5f63338aa7dfc2bf02843466e423b2bfa5a16dcfb00e12c048badff1ddb1f2c0ebd08afa3c04c2c18ee26f1596df22c231f08c70b6fcf2c2d4fae33861fdb2c3780173995a64b2c41b043dc05f9962c4be0341da2de7c2c560fe80137e8042c603f5f8990778a2c6a6e9ab977bcbd2c749d9993b8b5a52c7ecc5c1b1e2ea82c88fae25272c28f2c93292c3c80da8a2c9d5739dc12ae342ca7850b33f2439a2cb1b2a046e96f3d2cbbdff917c1d4162cc60d15a944e39f2cd039f5fe3bddd32cda669a196fd1332ce49301fda99acf2ceebf2dadb1e6452cf8eb1d2c512dc92d0316d07c4fba262d0d4247a075a2c92d176d829b8acdbf2d2198817056efbe2d2bc34421a18c262d35edcab231f5082d40181524cf4b292d4a42237c407e0c2d546bf5bb4c4bea2d5e958be4b941c62d68bee5fb4dbb662d72e80401cfe35d2d7d10e5fb05b30d2d87398be9b4f2af2d9161f5d0a339522d9b8a23b295ece62da5b2159252423d2dafd9cb729d3d0d2dba0145563baffa2dc428833ff23c972dce4f853285536c2dd8764b30b933fa2de29cd53d51ecbd2decc3235b135b382df6e9358cc12bf12e010f0bd51eda792e0b34a636efb1712e155a04b4f6ca912e1f7f2751f70ea42e29a40e10b335972e33c8b8f3edc6762e3ded27fe6917762e48115b32e74df12e523552942a5e772e5c590e24f40cc72e667c8de805ebd92e709fd1e0215de32e7ac2da1007945c2e84e5a67a798ffe2e8f0837223820cf2e992a8c0a03e6252ea34ca5349d4ea72ead6e82a4c498542eb790245d39d0872ec1b18a60bcd3fc2ecbd2b4b20d4ed42ed5f3a353eabc982ee01456491468412eea34cd94496c392ef455093848b2622efe750937d0f4182f0894cd95a0ba382f12b45654765d232f1cd3a3771004c32f26f2b5002ba88c2f31118af2870f892f3b302550dfd0582f454e841df351322f4f6ca75c7ec7ee2f598a8f0f3f3a092f63a83b38f17ca62f6dc5abdc5234932f77e2e0fc1dd6522f81ffda9b10a6192f8c1c98bbe6b7d72f96391b615bef392fa055628e2bffb22faa716e45126c742fb48d3e88ca88852fbea8d35c0f76b72fc8c42cc19c29ae2fd2df4abc2b63ed2fdcfa2d4e77b7cd2fe714d47b3b878f2ff12f40453105552ffb4970af12332c30056365bb98e313300f7d1f6d7eb6f93019969dc77d20c63023afe0cc4d625e302dc8e87ea88da53037e1b4e14784853041fa45f6e2f8ef304c129bc2336ce330562ab645f132733060429584d46bc7306a5a3981950b23307471a23eead2e9307e88cfbf8d55a030889fc20633f5f33092b6791595e6c0309cccf4f06a2b0f30a6e3359967962330b0f93b1344cb7330bb0f0560b83eb930c52494847833ee30cf39e8813abf5330d94f0159b5c57330e363df109efb2a30ed7881a8abe5a730f78ce92491da703101a1158705ff69310bb506d2bd4ad83115c8bd0a6c8364311fdc3830c840233129ef784884e8943134027d5456b4ab313e154756f1acd4314827d65309a9f331523a2a4b52556c315c4c43427f292831665e213b436f9831706fc4385243b7317a812c3c5e9115318492594a1b13d3318ea34b643a58ae3198b4028d6ebd0031a2c47ec86a6ec431acd4c017df6c9d31b6e4c67e7f85d831c0f491fefc5a7231cb04229c075b1931d513785851c93731df2293368cb6ee31e931733969072131f3401863976d7b31fd4e82b7c86e6c32075cb238ac5f3332116aa6e8f365e1321b7860cb4d795d322585dfe26a6168322f932430f9b6a13239a02db9aae28b3243acfc7f2d1f8f324db990842f79023257c5e9cb60cb293261d208576fc33f326bddec2b0adf723275e99548e06ef2327ff503b39e91ee328a00376df3399932940b307a8c2834329e15eedc16f10832a820729540f87432b22abba8b773eb32bc34ca192769fd32c63e9de93db25932d048371ba6f5cf32da5195b30fae5632e45ab9b224271632ee63a31b907c6232f86c51f2009bc3330274c6382043fa330c7cfff09b0506331684ff1e1c402633208cc3c34f27e1332a944de2dec00233349b9d7f75ddaa333ea2b29bbf27433348a98d3a6514963352b02d5e11eec0335cb693096fd0423366bcbe3f28a4fd3370c2af01e62a3b337ac8655451eeaf3384cde139155280338ed322b2d987463398d829c447901333a2dcf67008417533ace188b8c4417b33b6e5e0a12407ba33c0e9fe2bcfdd4e33caede15b6fdce333d4f18a32abf2b533def4f8b42bdc9633e8f82ce29729f633f2fb26c0953bdd33fcfde650cd44fc3407006b95e649a7341102b692871fdd341b04c749566f503425069dbcfab161342f0839f01a312c3439099be55b0b8734430ac39f632f09344d0bb120d85c0e34570c646c6024ba34610cdd849fecfd346b0d1c6c3cea9934750d2125dc2525347f0cebb422761134890c7c19b488ab34930bd25936da22349d0aee754db98e34a709d0709d47ec34b108784dc9782b34bb06e60f760f2b34c50519b846a3c334cf03134ade9ec734d900d2c9e13b0634e2fe5837f1855534ecfba397b25c9234f6f8b4ebc671a13500f58c36d0477b350af2297b72332b3514ee8cbc4e5bd5351eeab5fc06bab63528e6a53d3d1b313532e25a82931ac7353cddd5ceaa29283546d9172423882d3550d41e85a04be2355aceebf5c15a863564c97f77276c94356ec3d90c730cc23578bdf8b844980b3582b7de7d3c3dad358cb18a5df9ff303596aafc5d1db06b35a0a4347d46f78835aa9d32c1154d0635b495f72b27fbc135be8e81be1e20ee35c886d27c96ac2b35d27ee969305f7b35dc76c68689cf4a35e66e69d741627735f065d35df5524f35fa5d031d43aa9c360453f917ca49a2360e4ab55026e02136184137c8f6f1613622378084d7d332362c2d8f8666adec36362364d0407c7d3640190065020c61364a0e624747fdb33654038a79aec324365df878fed2a20b3667ed2dd94fb2613671e1a90bc1dec7367bd5ea98c4e48d3685c9f282f453b3368fbdc0cceb8eef3699b1557945cbae36a3a4b08a9e121f36ad97d2038f3d2c36b78ab9e6b3fa8a36c17d6836a6cab436cb6fdcf60200f636d56218275fc36a36df5419cd5a0b0236e945e1ea8aa38b36f33770818b2bad36fd28c594f514f6370719e12761a3d537110ac33b69efa9371afb6bd3a6e2ba3724ebdaf2b13a47372edc109b2186833738cc0ccf902a9c3742bbcf92955cbe374cab58e6c9261837569aa8cec362e1376089bf4d1bc258376a789c6469c6ce377467401744c5a6377e55aa6843e757378843db59fe2778379231d2ef0a54bb379c1f9129ff10f737a60d160d72d12a37affa619bfbdd7d37b9e773d830514837c3d44cc4a61b1637cdc0ec63f2fcac37d7ad52b8ac8b0637e1997fc5682e6237eb85738cbb224237f5712e113a756e37ff5caf557b09fb380947f75c11954c381333062792a018381d1ddbba92866e3827087817a577b93830f2db415f76c4383add053a5459ba3844c6f60517ca33384eb0ada43d453138589a2c1a581b223862837169fb6fec386c6c7d95ba3aec38765550a02746fb38803dea8bd53270388a264b5b566f2638940e73113d4282389df661b01bc57238a7de173a83e47738b1c593b3075fa438bbacd71c37caa238c593e178a68cba38cf7ab2cae4e0d038d9614b1583d56f38e347aa5b144cc938ed2dd09e26fcbd38f713bde14c6ed73900f97227150059390adeed7210e23b3914c42fc4d01932391ea93921e27db339288e098bd7bbf7393272a1053f53fb393c56ff90a8998c39463b2530a2b44539501f11e7bc9f94395a02c5b8852abf3963e640a58af8e8396dc982b15c810f3977ac8bde880e1a39818f5c2f9bbed3398b71f3a72585f33995545247b32a1f399f367813d245f239a918650e1047fd39b2fa1938fa72cb39bcdb94971ddcea39c6bcd72b0770e639d09de0f743ed5639da7eb1fe5fe4db39e45f4a42e7be2339ee3fa9c767b3f139f81fd08e6bd51e3a01ffbe9a8004a03a0bdf73ee2ff9893a15bef08c073f0d3a1f9e34769134893a297d3fb0590d843a335c123be9d1b33a3d3aac1bce5cfd3a47190d52915f7f3a50f735e2bd5d933a5ad525cedcafce3a64b2dd197983073a6e905bc51dd85f3a786da1d453853d3a824aaf49a433583a8c2784279960b53a96042070bc5fb53a9fe0842796570d3aa9bcaf4eb041d33ab398a1e892ef7c3abd745bf7c703e43ac74fdd7ed4f7503ad12b26804516723adb0636fe9f826d3ae4e10efc6c30d73aeebbae7c32ebc33af89615807b51be3b0270440bccd5d63b0c4a3a20aebf9d3b1623f7c1a82b2f3b1ffd7cf14009333b29d6c9b1fd1ee13b33afde066606053b3d88b9f1012d033b47615d7454d6d93b5139c892e71b273b5b11fb4f3de6313b64e9f5abdef8e23b6ec1b7ab4fe8d03b7899415016203e3b8270929cb6de273b8c47ab93b736393b961e8c379c10de3b9ff5348aea2b413ba9cba49026174d3bb3a1dc49d43bb33bbd77dbba78d3f23bc74da2e497f0533bd12331cab575f23bdaf8886f551ec53be4cda6d4fa79973beea28cfe28ea123bf8773aed63a8c33c024bb0a52dc31b3c0c1fee280a1b733c15f3f3787b69143c1fc7c0990438363c299b558c26ea083c336eb25465b4ac3c3d41d6f442a3483c4714c36e3f95fd3c50e777c4de41f13c5ab9f3faa031553c648c381206c3623c6e5e440d932c623c783017efc675b23c8201b3bb217dc93c8bd3177224f8363c95a44317516da63c9f7536ad273bee3ca945f2362696063cb31675b4cf840e3cbce6c12ba1e35a3cc6b6d49d1d666d3cd086b00bc194fe3cda56537a0dcc013ce425beea813da63cedf4f25f9af1603cf7c3eddbd9c3e43d0192b161bc67323d0b613cf3c162983d152f90946712b13d1efdac462ba96f3d28cb900b8d2e1d3d32993be7097d5e3d3c66afdb1e493a3d4633ebea4919183d5000f0170749ca3d59cdbc63d60d893d639a50d3326c023d6d66ad679942513d7732d2238743083d80febf0978f6393d8aca741beab96c3d9495f15d58bfb13d9e6136d03f119b3da82c4477198d493db1f71a5463e6633dbbc1b86a99a6263dc58c1ebc362b613dcf564d4bb4aa7e3dd920441b902d803de2ea032e43940e3decb38a864993723df67cda261cb69a3e0045f210375e253e0a0ed24713c05c3e13d77acd2be93f3e1d9feba4f9ba813e276824d0f6eb933e313026539d09a33e3af7f02f6577a03e44bf8266c96e3e3e4e86dcfc41fbff3e584dfff248052c3e6214eb4b5443e43e6bdb9f09df48183e75a21b306177933e7f685fc1530dfa3e892e6cbf2c1cd73e92f4422c648b913e9cb9e00b74177a3ea67f465ed253d03eb0447528f6a9be3eba096c6c5858633ec3ce2c2b6e74d53ecd92b468afea213ed75705269379553ee11b1e678fb97f3eeadf002e1b17b33ef4a2aa7cabd70f3efe661d55b810bb3f082958bbb5b3f23f11ec5cb11a86023f1baf29385c22523f2571be53effa633f2f341c064b55d73f38f64251e352713f42b831392ce41f3f4c79e8be9cd4f43f563b68e4a7c5353f5ffcb1adc22b583f69bdc31c6054083f737e9d32f6622a3f7d3f3ff3f84ee13f86ffab61d9e98e3f90bfdf7f0ed7d83f9a7fdc4e0a95af3fa43fa1d140754e3fadff300b239f403fb7be86fe2712613fc17da6acbda3e83fcb3c8f1959ff613fd4fb40466ea6be3fdeb9ba366df24b3fe877fcebca10bf3ff2360868f507373ffbf3dcb060b1424005b179c47ec0d9400f6edfa7c0be6e40192c0e5c9808e94022e905e575d5b0402ca5c644cb30a74036624f7d08fc3340401ea1909ff1454049dabc82009f54405396a0539b6c65405d524d07e0951240670dc2a1402c874070c901222a1c8d407a84088d0e258640843ed8e45bde77408df9722a82b5064097b3d461f1ed8540a16dff8d18a2ee40ab27f3ae65c6eb40b4e1b0c84821da40be9b36dd2e52ce40c85485ef86cf9440d20d9e01bfe4b640dbc67f1647b58340e57f292f8c3c0940ef379c4ffb492340f8efd87a0284784102a7ddb00f6c7d410c5fabf48f567b4116174349ef6e93411fcea3b29cb7c0412985cd31040bdc41333cbfc7921ba3413cf37b78b36eb54146aa0046d4639f4150604e34612fd6415a166543c5dfc54163cc45776e56c6416d81eed1c64f2f4177376155395a4f4180ec9d0432e073418aa1a1e11e20ef4194566fee663218419e0b072e76015141a7bf67a3b8530941b173915097c2c041bb2784377ec30a41c4db405ad79d9641ce8ec5bd0c732b41d8421460873bb241e1f52c47b1c63841eba80d74f5b8ee41f55ab7eabc913341ff0d2bab6fa3904208bf68b9781bc14212716f173efcb9421c233ec72d20a24225d4d7cbab38e3422f863a2721ce2242393765dbf940484242e85aec99c688424c99195b6b6f5e425649a12ad62092425ff9f25d4197414269aa0cf51567dd427359f0f4b8fe31427d099e5e939d634286b915350c5ff9429068557a8a37e0429a175f3173ee6b42a3c6325c30245642ad74cefd2551cd42b7233516b9c66d42c0d164ab53a94b42ca7f5dbd58f8f142d42d204f2f8b6b42dddaac633d0e4142e78801fbe7068242f135211b92d0c142fae209c4a5a12043048ebbf984834c430e3b37bc945a864317e77d1039e1a54321938bf6d9ab16432b3f6472d820e84334eb06869984c7433e96723481f002434841a77ef553924351eca66857781a435b976ef30bfdeb4365420121765d08436eec5cf5f9e5294378968272f9bdc2438240719ad8e602438bea2a6ffa34d6439593acf4c058f1439f3cf92b8dd8ce43a8e60f16c512b143b28eeeb8c83cac43bc379813f964a643c5e00b2aba705843cf8847ff6d1d5543d9304e9473010e43e2d81eec2d88d143ec7fb908fdf9d343f6271ced45712b43ffce4a9b64e3e14409754215bd1ee644131c035eaec71d441cc28e789a5961442668e365e02a8344300f0228e067524439b4eac3fb149b44435a9d39900f2e444d00198bff0be24456a55fbda7979944604a6fd0e917424469ef49c822c7dd447393eda5b3be7e447d385b6bfae8534486dc931d570aa344908094bc26c2d7449a24604ac8867944a3c7f5cb9aa33744ad6b5540fb3eee44b70e7ead4857a344c0b17212dfc38e44ca542f741f311b44d3f6b6d36426ec44dd9908330c03e044e73b239573ff1344f0dd08fcf927e444fa7eb86bf865f645042031e4ce7939450dc17569d7f9e245176282fd71587e4521035aa1f6dde8452aa3fc59c4ab54453444682736ba50453de49e0ca8dcc74547849e0c76bd074551246828fbddbf455ac3fc64939a0d4564635ac1992572456e028342678be64577a175e959b1cd45814032b8ca5407458adeb9b31407e945947d0ada913b46459e1b26319c347445a7b90bba8f124945b156bb77c3cc2445baf4356b9431ee45c491799859ec2245ce2e88006e7bc745d7cb60a62b3a7c45e168038be95a7a45eb0470b401e69245f4a0a820cdc23745fe3ca9d4a5a97f4607d875d1e231264611740c1adbc691461b0f6cb1eaafd24624aa9799670bae462e458cd3a8d1994637e04c6307d1c546417ad649dbb51a464b152a8a7bfd404654af49274004a1465e4932227efe6b4667e2e57e8ff69746717c633dc9d1e6467b15ab62834ded4684aebdef130110468e479ae5cf5a8b4697e042490ea27546a178b41b26f9c046ab10f05e6e5a3f46b4a8f7153a96a946be40c841e15a9d46c7d863e6b82aa346d16fca0614643446db06faa24b3db746e49df5bdb1c68c46ee34bb5a9ce70746f7cb4b7b61607c470161a62253cd3a470af7cb51c8a09747148dbb0c1426eb471e2375538a85994727b8fa2a7fbb1247314e4993479ed5473ae3639035e17547447848239e0c9e474e0cf74fd383124757a171172980b6476135b57bf31a8c476ac9c480833ebd47745d9e272cb497477df14272421c97478784b16415f065479117eafefa82de479aaaef4542001147a43dbe393e6d4947add057dd41a90d47b762bc339d6b2047c0f4eb3ea3448b47ca86e500a49f9d47d418a97bf2bfed47ddaa38b2dec26147e73b92a7b99d2e47f0ccb75cd41fdc47fa5da6d47ef34d4803ee61110a99bb480d7ee614c76ebc48170f35e205a74c48209f507b1551c7482a2f35e24655f54833bee619e87505483d4e61244b49964846dda703be47bd48506cb7ba90bcfc4859fb934b11d05648638a39b7908245486d18ab025bacc14876a6e72dc2034c488034ee3c1212e64889c2c02f9a421c4893505d0aa8d107489cddc4cf8bd95148a66af780914e3648aff7f52006fc8948b984bdb03a8ab948c31151337978cd48cc9dafac11207448d629d91c4eb4fa48dfb5cd867f435948e9418cecefb22f48f2cd1751ecc1ce48fc586cb7c30c354905e38d20bf0519490f6e788f2cf9e84918f92f055911ca492283b0858f4da6492c0dfd121b882549359814ad4975b6493f21f75964a4904948aba518b87cb34952351ded903ff3495bbe61da3709f449654770e0f7d030496ed04b041d61fb497858f045f268814981e160a8c166d6498b699c2ed4b9ea4994f1a2da769895499e7974adf1139a49a80111ab8e15a849b18879d597635e49bb0fad2e569b5049c496abb815360749ce1d75751c860749d7a40a67b5b7d149e12a6a9229d1e549eab095f6c1b4cd49f4368c97c61b1149fdbc4e777f994d4a0741db98369e254a10c733fc33724e4a1a4c57a5be38944a23d146971eedda4a2d5600d29d691e4a36da865a815b7a4a405ed73112502e4a49e2f35897ac9a4a5366dad358b0484a5cea8da39c74ef4a666e0bcba9ee744a6ff1554dc7eaea4a79746a2c3d129e4a82f74a694fe8134a8c79f60746c8094a95fc6d0867e97d4a9f7eaf6ef95db14aa900bd3d4110294ab282967584c6b64abc043b1a0a21704ac585ab2d169ac24acf06e6b0ef876a4ad887eda7da16774ae208c0141b51564aeb895df7f81bce4af509c755b534064afe89fc2f9732874b0809fc87e28a424b1189c860db888e4b1b095fbcc655324b2488c29de6f2634b2e07f106813cc84b3786eaf8d8eb814b4105b0773190254b4a844183ce96c94b54029e20f346004b5d80c650e2bee44b66feba15dffd114b707c79722dd6b14b79fa04680efc764b83775af9c5f9a84b8cf47d2995341b4b96716af9beec414b9fee246c853d214ba96aa9842a1c624bb2e6fa42ef5a474bbc6316ab16a1bc4bc5defebee1784f4bcf5ab280913e3a4bd8d631f2672e664be2517d16a45e664bebcc93ef89be894bf547767f5819ce4bfec224c85015f34c083c9eccb2336e4c11b6e48ebecd794c1b30f610b61a124c24aad354d829fb4c2e247c5d64e8c34c379df12c9c1cc34c411731c4bd67284c4a903e280843f04c54091658bc09f04c5d81ba5917eada4c66fa2a2b5af3394c707265d1c40a7c4c79ea6d4e91f2f24c836240a40349d44c8cd9dfd45687444c96514ae1c9fe524c9fc881ce9bdcfc4ca93f849d0a2c364cb2b6534f52cfe84cbc2cede7b386f74cc5a3546869eb454ccf1986d3b371b24cd88f852bcd6a234ce2054f72f4ff824ceb7ae5ab6737c64cf4f047d760f3f24cfe6575f91ef0164d07da7012ddc35a4d114f3626d9dffa4d1ac3c8374f934c4d243826467b05c34d2dac5056983af24d37204669e311904d409408829743784d4a0796a2f065b44d537af0cd29e8744d5cee17037f171d4d666109482b18444d6fd3c79d68edb54d794652057374774d82b8a8828564cd4d8c2acb16d952374d959cb9c4a9ab7a4d9f0e748e30baa24da87ffb75a8a5024db1f14e7d4b6b3c4dbb626da752e93f4dc4d358f5f8d64c4dce44106b76c4fd4dd7b4940a0623434de124e3d3e03a6a4dea94ffcb3e2f1f4df404e7f25901714dfd749c4b698cd44e06e41cd8a888244e1053699c4e85aa4e19c2829893f31e4e233167cfb119a94e2ca01943de1de94e360e96f752fff64e3f7ce0ec479b614e48eaf724f3a73d4e5258d9a38eb61a4e5bc6886a5036114e6534037b6f70c04e6ea14ad9238b534e780e5e85a386814e817b3e83263e964e8ae7ead3e26b6d4e9454637a0ea0804e9dc0a877e14cdc4ea72cb9cf90bb304eb09897835311cc4eba0441955e52a44ec36fb807e85b504eccdafadd26e5174ed6460a174f84e94edfb0e5b897ab694ee91b8dc334a4ed4ef28602395b99814efbf0431d418cee4f055a50711b5eb44f0ec42a371dca184f182dd0717d661e4f219743226ea5964f2b00824c25d7154f34698df0d724fb4f3dd26612b6957c4f473b0ab3f80a9c4f50a37bd6cf42344f5a0bb97d6fd5f74f6373c3aa0d3b734f6cdb9a5edac4164f76433d9e0b9d304f7faaad69d2cff64f8911e9c46341834f9278f2afefb2e14f9bdfc82eaac1064fa5466a42c6e4d94faeacd8ee7673374fb8131433eb9cf54fc1791c15586ee04fcadef094eed1c74fd44491b4e08a754fdda9ff775f39bd4fe70f39de9c5c774ff07440ecc94b844ff9d914a4173bd650033db506b73e6b500ca22216da40575016065bd6b10ac2501f6a62486c42ef5028ce356e3c6a3e503231d54a51de2e503b9541dedcd8605044f87b2e0d6e9b504e5b813a1392ce5057be54051f1316506120f3915f99bb506a835fe104ad3b5073e598f63db047507d479ed339e1c85086a9717a285ce250900b10ed3818f750996c7d2e97e9ab50a2cdb640767ee550ac2ebc250264d450b58f8ede6a03f150bef02e6edba10350c8509ad8855d2050d1b0d41d9535b350db10da4039047c50e470ad429e7f9550edd04d26f3397650f72fb9ef64a0f351008ef39e2001465109edfa3552820e51134ccdb7292751511cab6e25d0d183512609db83763d84512f6815d24604a95138c61d146c9cb8514223f14c1657f2514b81927b6f65125154df00a4a3cf50515e3c3bc9df7e6651679943ed4e368f5170f619111b9892517a52bb377321bd5183af2a62802be9518d0b66946ded815196676fcf677986519fc3461597bf8a51a91ee969298bba51b27a59cc4786e051bbd597411c366251c530a1c9d1fc4d51ce8b796893174e51d7e61e1f89a2bc51e1408ff0df969c51ea9acedebec79c51f3f4daeb50e71d51fd4eb418bf83365206a85a693406b4521001cdded7b91b52195b0e7bd3beb15222b41c42511878522c0cf73478a4385235659f54731c7e523ebe14a469189e5248165726830cbc52516e66dce949c7525ac643c9c3fd8452641dedef3b3288526d75654f76d0475276cca9ec9e9b07528023bbc8da33f452897a9ae65119175292d147472aa55d529c27c0ed8e109b52a57e07dba26f8f52aed41c138eb3e552b829fd9779ac3752c17fac698a041352cad5288be643fd52d42a7200b4d17252dd7f88ca1beee952e6d46cea41bbd952f0291e634c34ba52f97d9d3761330b5302d1e968a66d4e530c2602f9417714531579e9eb57c0f8531ecd9e410e98aa5328211ffc8b28e85331746f1ff27989533ac78bad696f8053441a75a714ccd7534d6d2d0f1930bc5356bfb1e79b177b5360120432beda8953696423f2a8b0825372b611297cad2c537c07cbd95ec17a538559540472bb94538eaaa9acdc46d35397fbccd4beebc853a14cbd7e3e103d53aa9d7bab7cf73953b3ee075e9ec10153bd3e6099c66b2253c68e875f16d06853cfde7bb0b2a8ed53d92e3d90bc8a1353e27dcd0156e68b53ebcd2a04a40e5953f51c549cc62ed553fe6b4ccbdf52ab5407ba12941161e5541108a5f77e21e8541a5706f847357a5423a535988e1cc5542cf331da743555543640fbc01aba23543f8e934ba2c3935448dbf87f2d47745452292b5cdb190a545b762be6cce90d5464c2fa1f2345ad546e0f9607fe9a9254775bffa37f30e45480a836f3c52f4a5489f43bfaf099ed5493400ebb21527c549c8baf3677183254a5d71d6f1187d354af225967101bb254b86d6320922bb454c1b83a9db6ed5354cb02dfe09d73a054d44d52eb64af4654dd9793c02b6e8f54e6e1a261105d6154f02b7ed032054b54f975290faecd7e5502bea121a4fad3550c07e70832afd2551550fac575ecb1551e99dc5b8c8f555527e28bcc94535b55312b091aaad214553a735447ed828d5543bb6d5679b991554d0354486ca9a955564b091fe36322555f928bdefad4105568d9dc87cfc84d557220fb1c7ee97f557b67e79f24bf1d5584aea211ddae6f558df52a76c5fa8d55973b80cff9c46a55a081a51f950ad455a9c79767b3aa7255b30d57aa715dcd55bc52e5e9e9bd5155c5984228383f4d55cedd6c677837fc55d82264a9c4d98355e1672af13933f355eaabbf3ff0355055f3f0219804a99055fd3451fb913aa2560678506cb0706f560fbc1ced7cb0da5618ffb780103fc55622432026853f19562b8656e2f5aebc5634c95bb77b6ca5563e0c2ea63034cf56474ecfb12da1445650913eda8d2a215659d37c246825955663158790d7c7e3566c576121f5236956759908d9d928a4567eda7eba9ca62956881bc2c65848b356915cd4ff249b22569a9db5671a067a56a3de640050d1ec56ad1ee0cce122d456b65f2bcee2fcc056bf9f45086e416f56c8df2c7b9ab0d656d21ee22a7fe92156db5e66173566bb56e49db843d2844a56eddcd8b26e7ab356f71bc76520612257005a845dff2d0b5709990f9f21b2275712d7692a9ea27e571c1591028c8e65572553872901e485572e914ba014f1db5737cede69dbe1ba57410c3f886cbdd3574a496efddd6e305753866ccc43b93d575cc338f5b543c95765ffd37c479109576f3c3c6210029957787873a923d8815781b47953983138578af04d638209a357942befdaf63d1e579d6760bc09857c57a6a2a008d07b0557afddadc35f948257b91889edcb273757c253348a2766ee57cb8dad9a8865f257d4c7f52102151757de020b1fa843ba57e73bef988e9fc857f075a28dc8b5ba57f9af240169f09e5802e873f5859a18580c21926c2eda6158155a7f6778b851581e933ae976195c5827cbc4f439c1975831041d89d653b8583a3c44ac5e511e5843743a5de419d4584cabfea079ec885855e3917631e69f585f1af2e11e042c58685222e3501ff6587189217ed9f37b587abfeeb5cd16f45883f68a8a3b0155588d2cf4fe3508545896632e13cc6064589f9935cd121cc358a8cf0c2c172f7358b204b132ec694158bb3a24e3a279c758c46f674049ef7258cda4784af3377d58d6d95805ae9dfc58e00e06728c4ddb58e94283939c50df58f276cf6aee8fae58fbaae9fa92d1cc5904ded34498bda1590e128b4b0fd87f591746121007869b59207967958f0b185929ac8bddb5880a5932df7eea89fe74593c1240be1b4e4c594544d15a783683594e7730c1af54fe5957a95ef5cf26a15960db5bf8e60752596a0d27cd0231f659733ec27431c073597c702bf082abbe5985a1644402cbcf598ed26b70bfd7b15998034178c7657a59a133e65e26ea5459aa645a22ebba8059b3949cc923095459bcc4ae52d9e94159c5f48ec21d4bd759cf243e18fa01c559d853bc597cbadd59e1830985b2061859eab2259fa6519359f3e110a965ea9d59fd0fcaa4fcfdad5a063e539477966c5a0f6cab79e19fb85a189ad25746e3a45a21c8c82eb30b7b5a2af68d02319fc55a342420d3ce08475a3d5183a5938c085a467eb5798d51515a4fabb651c65db45a58d8863049960b5a6205251721be7b5a6b319308597a775a745dd005fb4cc85a7d89dc121197865a86b5b72ea69c215a8fe1615dc47b645a990cdaa17535765aa23822fbc2a9da5aab633a6eb6977b5ab48e20fc5a9ca15abdb8d6a6b837005ac6e35b6fd8c3b35ad00daf59c57f475ad937d2668785b05ae261c49827d25a5aeb8b85f0af40235af4b516722689635afdde761e9647e95b0707a4f806f5045b1030a30080e97f5b1959703a0c5daa5b22820ca6b169585b2baa78487803e65b34d2b3216804395b3dfabd338920c55b47229680e2ef8c5b504a3f0b7ce6235b5971b6d55e59b45b6298fde08e7f045b6bc0142f146a6c5b74e6f9c2f70fe85b7e0dae9e3d43125b873432c2edb7265b905a86330eff055b9980a8f0a78d3b5ba2a69afdbdb3fb5babcc5c5c57a5275bb4f1ed0e7b72515bbe174d162f0cbe5bc73c7c757845695bd0617b2e5ccd035bd9864942e233f95be2aae6b50dea775bebcf5386e540675bf4f38fba6d65765bfe179b51ab69185c073b764ea43a875c105f20b35ca8cb5c19829a81d962b75c22a5e3bc1ef6ef5c2bc8fc6431d3ea5c34ebe47c1647f45c3e0e9c05d081335c47312303648da55c50537976d65b285c59759f6229b77a5c629794c762503b5c6bb959a883b2f15c74daee07914d0a5c7dfc51e68e6bdf5c871d85477e3cb75c903e882c63cccb5c995f5a974209425ca27ffc8a1bbf3b5caba06e06f39bd15cb4c0af0fcc2c135cbde0bfa6a7dd145cc7009fcd88fbe25cd0204f8671b5915cd93fced364173a5ce25f1db6620dfe5ceb7e3c316d67065cf49d2a4687cf8e5cfdbbe7f7b2d4dd5d06da7546efe44f5d0ff8d236404b565d1916fec7a5377b5d2234fafd1fb6615d2b52c6d8b0b5cb5d3470625c5903975d3d8dcd8a194dcc5d46ab0863f222905d4fc812ebe3f0375d58e4ed23ef053a5d6201970e1390435d6b1e10ac51a0285d743a5a00a923f55d7d56730d19eaeb5d86725bd3a3a4805d8f8e145645e0655d98a99c97000e8b5da1c4f497d17f1f5daae01c5ab962925db3fb13e1b6c9985dbd15db2ec8a52f5dc6307243edc69d5dcf4ad92324df775dd8650fce6c819e5de17f1647c31f485dea98ec91270aff5df3b292ac9677a35dfccc089c0f786d5e05e54e619000f65e0efe63ff15e5335e181749769ed97c5e212ffeca28728c5e2a4883fbb025865e3360d90d3347f75e3c78fe00af0fd75e4590f2d820938b5e4ea8b79584c9ef5e57c04c3ad88a4d5e60d7b0ca188c685e69eee54541687d5e7305e9ae4f97425e7c1cbe073f71f05e853362520d323b5e8e49d690b4f2615e97601ac532ad1f5ea0762ef1823dc25ea98c13179f601c5eb2a1c73985b0925ebbb74b5930ac175ec4cc9f789bb0315ecde1c399c1fafe5ed6f6b7be9eab305ee00b7be92cc01a5ee920101b6719a65ef23474574878645efb48a89ecb7d825f045cacf3eaaad65f0d708158a062dd5f168425cee6e8bf5f1f979a58b860515f28aadef80ece155f31bdf3aee417455f3ad0d87f3201cb5f43e38d6af234475f4cf612741e361a5f5608679caf6f5b5f5f1a8ce69f28e15f682c8253e68c495f713e47e67ea3f05f7a4fdda0605afa5f83614383847d575f8c727991e3b7c15f95837fcd7697c05f9e945638358bb05fa7a4fcd418e2be5fb0b573a318ccee5fb9c5baa72d5b1e5fc2d5d1e24e7f065fcbe5b956740b3c5fd4f5710595b3375fde04f8f1ab0b505fe714511cab88c95ff02379888e81c95ff93272374b2d616002413b2ad8a393600b4fd4652ddd4e60145e3de841b475601d6c77b60ae3df60267a81d080075a602f885c39979bb060389606f347fea76041a381ff876f03604ab0cd604c0c8c6053bde9178bd80b605ccad5273cb3546065d79191546141606ee41e57c885ba6077f07b7c8ea5b46080fca9019c2737608a08a6e8e6515d6093147534624c56609c2013e605216b60a52b82ffc3bb0360ae36c28392e49c60b741d273674add60c04cb2d1357b8860c957639ef1e58b60d261e4de90d8f660db6c369206870760e47658bb47022860ed804b5c463df360f68a0e76f80f3560ff93a20d502beb61089d0621422b4f6111a63ab4c185d1611aaf3fc9c1951f6123b81562359424612cc0bb80109f0d6135c9322545b34b613ed17953c7af966147d9910d8953ee6150e179547d419b6159e9322a95fb3a6162f0bb91c5e4b2616bf8158bff43406174ff401b343d76617e063b4156db3c61870d07005905d6619013a35a2c87e761991a1050c30d6e61a2204de60e23cd61ab265c1bff39ca61b42c3af4879f9461bd31ea719886c161c6376a9523025561cf3cbb611806c061d841dcd76869e761e146cefa04e31e61ea4b91cade0b3461f350254be45c6a61fc54897f083282620558be6639cab9620e5cc4036943cc6217609a58869dfb622064416781bb09622967b9324a5e4562326b01bad02c83623b6e1b0302ac27624471050cd14523624d73bfda2b40f96256764b6cffcac3625f78a7c73def2b62687ad4ead49c7a62717cd2d9b2a292627a7ea195c6b2f36283804120ff60be628c81b17d4b20b6629582f2ac984943629e8404b0d5127862a784e78bef960e62b0859b3fd5cf6e62b9861fce759baf62c2867539bcb99a62cb869b8398c9ae62d48692adf74e1d62dd865abac5aad662e685f3abf1258262ef855d8366e58962f884984313f411630183a3ece53c07630a828082c78a1c6313812e06a78cc7631c7fac7a71d44e63257dfbe012d2c0632e7c1c3976dbfe63377a0d888a25b9634077cfcf38c778634975630f6eba9a635272c74b17da53635b6ffc841fe3b663646d02bc7275b6636d69d9f5fb11246376668232a518b2637f62fb745bd0ff63885f45bd0a608a63915b610e9bcfc2639a574d6afb090163a3530ad412d89063ac4e994bcdecac63b549f8d416d58463be45296ed8054163c7402b1dfbd00263d03afde36c6be263d935a1c113f0fd63e23016b8dc596e63eb2a5cccaf815263f42473fe7726cf63fd1e5c501cea0e64061815c38a4d45640f11a05aa8b4ba64180afc176166be64210428fb9d8bb46429fd2709462e156432f5f642443a71643bee96a8807f6f6444e7083de3add2644ddf4b0456587f6456d75efdc0f474645fcf442c0bd8d96468c6fa911f3ef76471be822ee34240647ab5db073fe0506483ad051c1cf8f0648ca4006f624e1764959acd02f783ef649e916ad8c420d464a787d9f2af8d5a64b07e1a52a1144e64b9742bfa7fe2b564c26a0eec3307d464cb5fc329a1753164d45548b4b1fe9364dd4a9f8f4b5a0564e63fc7bb541fdb64ef34c13ab2cab264f8298c0f4db77265011e283b0b2555650a1295bfd135e0651306d49f85ecf2651bfae4dc0f30bb6524eec67752c9c4652de279733662f36536d5fdd19f8988653fc9539473ad256548bc7abd981fcc6551af734ef215e5655aa23d4a66a63b656394d8b1daca07656c874587335cea65757983cc551cf3657e6b938324aaa365875d74ad8688ee65904f274d5f1d3a659940ab6492af6765a23200f50569cf65ab2328009b594665b4142089386d2165bd04ea90c0773565c5f58619172bdc65cee5f3242021f465d7d631b3bed2e265e0c641c9d69a9a65e9b623684ab79865f2a5d690fe4aec65fb955b45d45835660484b188afc5a8660d73d95b735c0f661662d2c001c6cd661f519db83d93e26628403a460933ea66312ea86b46fa22663a1ce829d91c6a66430af983a1b347664bf8dc7a82b9e36654e691105e0e16665dd4174715705f6666c16f208a83ee666fae989e9ecea566789b93c333b91866818860902a8e90668a74ff07647d0f6693616f2ac2954f669c4db0fc25cacb66a539c47d6ef3bb66ae25a9b07ec91566b711609735e69866bffce93374cac766c8e843871bd6ee66d1d36f940b4f2366dabe6d5c235a4966e3a93ce144021566ec93de254d330d66f57e512a1ebc8b66fe6895f19850bd670752ac7d9984b067103c94d001d0486719264eeab08e4967220fdacf84fc56672af938805e3af56733e267ff1b4d90673ccb694d9b1a7b6745b43c6dbc6af2674e9ce1615deb1a675785582a5e2a0d67606da0ca9b99cf676955bb43f48f5a67723da7984742a0677b2565c971ce8667840cf5d95230ed678cf457c9c64ab46795db8b9cabdfb8679ec29153e096d367a7a968f141f9eb67b0901276ad75e467b9768de6005aae67c25cdb4117db4267cb42fa89d10da767d428ebc208eaf467dd0eaeeb9c4f4e67e5f4440867f9f367eed9ab1a488d3267f7bee4231a8e766800a3ef24ba6644680988cc2104604068126d7b19d4ab27681b51fc110758e26824364f08785e76682d1a74020394126835fe6aff84b50d683ee23402d75feb6847c5cf0dd7165b6850a93c225f3d3e68598c7b424b1ca668626f8c6f75dfda686b526fabba955668743524f8f42ed4687d17ac58fd81456885fa05cdb144da688edc3158ea15036897be2efc82707268a09ffeba54b92268a981a0943b344e68b263148c100a8168bb445aa3ad478f68c42572dcecda9968cd065d39a8961468d5e719bbba2fc368dec7a864fb40c268e7a8093745458268f0883c34719dcf68f968415e598ccf69024818b6d6390b690b27c23fc0ac666914073dfaf1d42a691ce68bea4281066925c5ac0f8b670f692ea49e6ca51dc66937836303682016694061f9d5accc5869494062e54b645a69521e9e341c0d58695afcabc3f6d0056963da8b96b3988d696cb83dae2a3695697595c20c325d3e697e7318b2a3a32669875041a355827069902d3ce01f58be69990a0a6ad8673869a1e6aa4557d29269aac31c7174a30569b39f60f105c45769bc7b77c5e205de69c55760f1e01a8269ce331c76d698bb69d70eaa569bfa9a69dfea0a93069dc369e8c53d2decc37869f1a0422924909869fa7b1986840d9d6a0355c347e126a46a0c303f6f11ab6d6a150a8dfdeb4f5c6a1de4aef643a9806a26bea259f0348f6a2f98682ac64eed6a3872006a9b3aaa6a414b6b1b441d8b6a4a24a83e9601056a52fdb7d665d2436a5bd699e48862276a64af4e6ad2654f6a6d87d56b1874146a76602ee72f0a8f6a7f385ae0ea88956a8810595a1f31c46a90e82a54a12d7b6a99bfcdd24486e46aa29743d4dd2cf16aab6e8c5e3ef25e6ab445a7703d8dbb6abd1c950cac99626ac5f355355f93846acec9e7ec29de286ad7a04d32debf276ae076850b51603d6ae94c8f7754cef76af2226c78bbfcc96afaf81c1159bf036b03cd9e4300ced86b0ca2f30f83c9636b15781a78b52fa26b1e4d148067667e6b2721e1286cb6ce6b2ff68072974d566b38caf260b93ac86b419f36f4a473c96b4a734e302ad0f76b534738151e0ee16b5c1af4a54fce156b64ee83e29193166b6dc1e5ceb4c66c6b76951a6b8ab4996b7f6821bae48e246b883afbbe93679a6b910da87868398b6b99e027ea33e0936ba2b27a15c71d586bab849efcf2948c6bb45696a186cef46bbd2861055439656bc5f9fe2a2b24c96bcecb6e11dbc61f6bd79cb0be3636806be06dc6310a731f6be93eae6c285d4d6bf20f69715fba7a6bfadff7428034376c03b057e159583a6c0c808b4fba985f6c1550918f734aa96c1e206aa252a9466c26f0168a27d2926c2fbf9548c1c9166c388ee6dfef738e6c415e0b517f9ce96c4a2d029f40f44b6c52fbcccb020d106c5bca69d6915ecf6c6498d9c3bd455b6c6d671c945400c36c7635324a23b55c6c7f031ae6fa6bba6c87d0d66ca610b76c909e64dcf475746c996bc639b34f606ca238fa84b0382d6cab0601bfb8ade56cb3d2dbec9a12da6cbc9f890d21adb76cc56c09231ca9786cce385c305815716cd7048236a0e54e6cdfd07b37c3f11a6ce89c47358df5396cf167e631cb92706cfa33582e494de96d02fe9d2cd3912f6d0bc9b52f36aa346d1494a0373ecb556d1d5f5e46b80b556d2629ef5f6e656a6d2ef453832db9336d37be8ab3c1cac66d408894f2f642a96d4952724296adda6d521c22a46e7dce6d5ae5a61a4908746d63aefca5f188376d6c782649331c026d75412305d8c7416d7e09f2ddad71e16d86d295d27be8576d8f9b0be60edb9c6d9863551a30e1366da12b7170ac73356da9f360eb4bf0376db2bb238bd99b6b6dbb82b9541f9c906dc44a2245e7ffff6dcd115e62fcb6a06dd5d86dad2795fb6dde9f502632582f6de76605cfe69bf96df02c8eac0de4b86df8f2eabc719a696e01b91a02db09ae6e0a7f1c811363d16e1344f238e3bec26e1c0a9b2c15151d6e24d0175c7046286e2d9566cbbe15db6e365a897bc72cde6e3f1f7f6e5418896e47e448a52d4aee6e50a8e5221b1ad46e596d54e6e5c3bb6e623197f55565e16e6af5ae4f32063f6e73b997f6438e916e7c7d54ec51cd546e8540e5332475cb6e8e0448cc831ffb6e96c77fba3548b96e9f8a89fe02519f6ea84d6799b181176eb110188f0a02596eb9d29cdfd2e56f6ec294f48dd31f386ecb571f9ad189686ed4191e0894e2896edcdaefd8e3ce026ee59c950d84d4146eee5e0da83e61e06ef71f59aad6c9686effe0791714418f6f08a16beebce61f6f1162323396b7c66f1a22cbe7679c206f22e3390bf55db06f2ba379a305abe86f34638dae5e1b2a6f3d23752fc424cc6f45e33028fd27146f4ea2be9bce653f6f57622089fd07866f602155f54e1b176f68e05edf8692226f719f3b4a6b43d26f7a5deb37c0ec546f831c6ea94c2cd96f8bdac5a0d18b956f9498f0201573c76f9d56ee28dc35b36fa614bfbcea06ab6faed264de0301116fb78fdd8deb24526fc04d29ce6654f16fc90a49a1385c836fd1c73d0824e9b56fda840404ef904b6fe3409e995bc9246febfd0cc72cf23d6ff4b94e90264eaf6ffd7563f60b06b97006314cfa9e27b7700eed099fa2a42e7017a899e6db53cb702063fdd20af36170291f3562f424f47031da409b596faf703a951f7cfd3ff470434fd209a1e752704c0a5843099c8d7054c4b22af67ba3705d7edfc32a85c4706638e10d67a162706ef2b60b6f9a257077ac5ebf0420f8708065db29e6cc0470891f2b4dd916b87091d84f2c9c61c8709a9146c7f1f32b70a34a12219af62870ac02b13b587b4a70b4bb2416eb787070bd736ab614c8c770c62b851a952ccd70cee373462d4a5270d79b353a9dac8170e052caf9a6c3da70e90a348508e63770f1c171de844ecf70fa788307d91e3971032f6802c75a6b710be620d10eeebd71149cad746fabf1711d530deea9482771260942417b5ef1712ebf4a6ea571467137752677e6e58f71402ad65eff07a17148e05a25ad08c2715195b1cdafffaf715a4add58c6e8997162ffdcc8b0a527716bb4b01f2bfc7d717469575df79b3a717d1dd286d213797185d2219b79dcd7718e86449dad547571973a3b8f2abcf4719fee0671b03e8171a8a1a546fbe6cc71b1551810cba91371ba085ed0dd5e2171c2bb7988eec44e71cb6e683abd7f8471d4212ae807194071dcd3c19289009471e5862c3c008a2871ee386ae62af04071f6ea7d92c552b971ff9c64438cb71072084e1efa3e085e7210ffadb89617607219b11080519a7772226247532d2da9722b135232e552a37233c431213670bd723c74e41fdcd4fd7245256b3094b214724dd5c6551a2063725685f58f291e01725f35f8e07d8eb57267e5d04ad33bff7270957bcfe5d517727944fb7170eef07281f44f31300439728aa37710de755f7293527312378893729c014336f669c572a4afe780d62aac72ad5e5ff191c2c572b60cac8ae40f5872bebacd4e87d37572c768c23e37b7fd72d0168b5bae4b9e72d8c428a8a602d772e1719a26d937fc72ea1edfd8022b3572f2cbf9bddb028172fb78e7da1dc9be730425aa2e84729d730cd240bcc8d4b473157eab86a4ad75731e2aea8dd1a0367326d6fdd409362d732f82e55b04de7d73382ea1247dee297340da31322da0247349859585cd154b735230ce21155469735adbdb05bf4a39736386bc3583c96b736c3171b21b8a9d7374dbfb7d3f2c6b737d865998a733677386308c060c0a19738eda92c726010c7397846dddad4ec573a02e1d4b5a0fcc73a8d7a111e446ac73b180f93303dbf373ba2a25b0709e3773c2d3268be2421773cb7bfbc710623b73d424a563b27f5a73dccd236380003a73e57575c83031b073ee1d9c937a46a773f6c597c715581c73ff6d6764b865267408150b6e1a52f27410bc83e4f1eccb741963d0caf5e41874220af221dcd060742ab1e7eb5d2f4a743358b2292d64a2743bff50dd03ba577444a5c408966083744d4c0bad9b6d667455f227cdc8dd6d745e98186ad4933474673ddd86745782746fe377225dd954747888e54046adda74812e27e1e450787489d33f08ec22ca7492782ab7136ca6749b1ceaee0f5c1e74a3c17faf95058174ac65e8fd59635f74b50a26d911568874bdae394471a61274c65220412eff5774cef5dbd0fdf5fa74d7996bf59303e674e03cd0b0a2895474e8e00a03e0ccca74f18317f101fb1d74fa25fa79ba27767502c8b19fbd4b52750b6b3d64bf468075140d9dca73df2b751cafd2d28ec1d6752551dc7ec38163752df3bad0c5970e7536956dca486274753f36f56cff29977547d851ba9d18d975507982b4d5430475591a885d5aa14a7561bb62b5e01348756a5c11c0185f057572fc957db630f8757b9cedf06c1c0675843d1b19ec9988758cdd1cfbea094a75957cf39816b191759e1c9ef024bf1375a6bc1f05c6450975af5b73daad3d2075b7fa9d708b878a75c0999bc912eaf375c9386ee5f5148f75d1d716c8e3981075da7593738fefb575e313e4e7ab7c3e75ebb20b26e784fa75f4500632f537c175fcedd60d85a8fd76058b7ab849d3a3760e28f434f2993d7616c6428530c1ea761f6365aab4fc5c7628005da72fdde076309d2a7c51e25c763939cc2bcb6c507641d642b74cc4dc764a728e20861bc276530eae69278760765baaa392e104bd7664466d9f627785766ce20c905baa0c76757d80677c4d4e767e18c92673f8f37686b3e6cef22b55768f4ed962a649797697e9a0e33f9f1776a0843d526d5e9c76a91eaeb1dea12b76b1b8f50342669e76ba53104847958776c2ed00829cfb3676cb86c5b3f14bba76d4205fddf321dd76dcb9cf0250ff2e76e5531322b94c0076edec2c40da576a76f6851a5e62574a76ff1ddd7cff684b7707b6759e5f8ddf77104ee2c430b2487718e724f020a69777217f3c23dd22ae772a17286113c5437732aee9a97213e0773b467ffea57ae97743ddeb625b4d99774c752bd640c60677550c415c030525775da32bf54f12c8776639eba3d1dda3776ed08069383b4e777766ea472ee845777ffd293f6287ec7788933d537fa48f779129268532af637799bee4d628008e77a25478480bd72377aae9e0dc8a592477b37f1e954f938a77bc143174077a3c77c4a9197a5de81f77cd3dd6a9fe9f0b77d5d269049547d677de66d08bcd725077e6fb0d4152954a77ef8f1f26d00e9377f823063df122fe7800b6c28860fe6178094a5407cab39b7811ddbabdd93c8e781a70f6ac377a2a78230407d490346c782b96ee388e1a5c783429a9d9dbc214783cbc3aba23a8be78454ea0db103299784de0dc3e4baafc785672ece5804451785f04d2d25818207867968e067d27097870281e839958ce7878b9844b567c4b78814abf5f5e47827889dbcfc15a579778926cb572f430d1789afd7075d53ea378a38e00cba6d3a378ac1e667612299878b4aea176c0617278bd3eb1cf5a835378c5ce9781897e8b78ce5e528ef6299f78d6ede2f949424878df7d48c22b6d7578e80c83eb45374e78f09b94763f133678f92a7a64c15bcd7901b935b87452ef790a47c6730021bc7912d62c960cd893791b646823426f187923f2791c48c437792c805f82c79e2279350e1b5866aa54793d9bac9ecd7d967946291357a393ff794eb64f849050f579574361273aff2d795fd048414ad0b479685d04d466deea7970e996e2362a87797975fe6c5f9b9a7982023b748a0192798a8e4dfc5c133679931a36057c6eb1799ba5f39191998a79a43186a24200ae79acbcef3933f86e79b5482d580dbc8179bdd3410075700979c65e2a34111d8f79cee8e8f486b70a79d7737d437c15e179dffde72296fae979e88826937d0e6879f1123b97d3e01d79f99c263140e7387a0225e6616982617a0aaf7c29f2f7be7a1338e78c8274ed7a1bc2288abd0f0b7a244b3f2647c2b37a2cd42b60c774037a355ced3be0ee9c7a3de584b938e5a47a466df1da73f3c77a4ef634a1369b3a7a577e4d0f2545c07a60063b25e444a47a688dfee717d0c47a71159854640a8a7a799d076f6cf9f87a82244c39d68ea07a8aab66b5449fac7a933256e35aebdd7a9bb91cc5bd198d7aa43fb85e0eb6b77aacc629adf338ef7ab54c70b70dfd697abdd28d7b0248fb7ac6587ffb7348227acede483a040efc7ad763e63857994e7adfe959f810ca897ae86ea37ad26dc87af0f3c2c23f35d17af978b7cff9bd1c7b01fd82a5a485d07b0a822344e1f9c57b130699af546a8a7b1b8ae5e69e11627b240f07ec610f4b7b2c92ffc23f6cf97b3516cd69db1ae27b3d9a70e4d5f1337b461dea34d1afdd7b4ea1395b6ffe917b57245e5a526cc47b5fa759331a71b07b682a29e7696c577b70acd078e0a3827b792f4ce92145c77b81b19f39cc69897b8a33c76c830cf77b92b5c582e616127b9b37997e9652b17ba3b943613478787bac3ac32c6124e87bb4bc18e1bcdd577bbd3d4482e80ef27bc5be4611830ec87bce3f1d8f2e19c27bd6bfcafd8954a87bdf404e5e34cc257be7c0a7b2d074c67bf040d6fcfc2afe7bf8c0dc3e57b3267c0140b77882b9817c09c068ad1cd23b7c123fefddc5796f7c1abf4d0c1c13237c233e8039bfeb517c2bbd89685035e27c343c68996c0eb47c3cbb1dceb2799c7c4539a909c262667c4db80a4c3a9cd57c56364197b9e4ac7c5eb44eeddedda87c673232504813857c6fafebc093fa027c782d7b4060ece07c80aae0d14d2fe37c89281c74f6eed77c91a52e2cfc3d917c9a2215fafb17ef7ca29ed3e09161db7cab1b67df5ce74d7cb397d1f8fb5c4e7cbc14122f0a5cf67cc4902883276d757ccd0c14f6effa0a7cd587d78c0157107cde037043f8c0f97ce67edf20735c527ceefa24230e35c57cf7753f4d66421b7cfff030a1185e3c7d086af81fc14f337d10e595cafdc22e7d196009a46a4c827d21da53ada36bac7d2a5473e84585527d32ce6a55ece7447d3b4836f835c7817d43c1d9d0bc44387d4c3b52e11c63c47d54b4a22af214b97d5d2dc7afd92ddc7d65a6c3716d6e287d6e1f95714a7cd17d76983db10be9467d7f10bc324d2b317d878910f6a9a2787d90013bffbc97457d98793d4f2139ff7da0f114e672a3527da968c2c74bd42e7db1e046f347b5cb7dba57a16c0119a97dc2ced23312b9937dcb45d94a1737a17dd3bcb6b2a91e377ddc336a6e62e00a7de4a9f47eded8207ded2054e5b749d67df5968ba48660d97dfe0c98bce631317e06827c3070b73e7e0ef83600bfd7bd7e176dc62f6d5fc37e1fe32cbe1304c87e285869ae4a64a07e30cd7d01ad05877e394266b9d456177e41b726d859ad547e4a2bbd5ed64aa77e52a02a4ee355e37e5b146daa19df497e6388877212df817e6bfc77a86737a77e74703e4eafb1477e7ce3db6684fe5d7e85574ef17fb95b7e8dca98f138652b7e963db967476d2a7e9eb0b0554525327ea7237dbcc9c9997eaf96219f6d7f307eb8089bfec853497ec07aecdc723bb57ec8ed143a0316cb7ed15f121912ab647ed9d0e67b38a8de7ee24291620ca7227eeab412cf2626a27ef3256ac41c905a7efb9699428735d77f04079e4bfd51337f0c7879e21605187f14e92c06685cc67f1d59b4ba8b4c107f25ca140015af5e7f2e3a49d89e4bb47f36aa5645bbcead7f3f1a394904ce807f4789f2e40fca047f4ff983187328ae7f5868e9e7c53a947f60d827539c386f7f69473b5d8e439d7f71b626073166237f7a24e7521b92b17f82937f3fe2a49b7f8b01edd21c5fe57f9370330a5e71427f9bde4eea3e6e147fa44c417351d46a7facba0aa72e0b0c7fb527aa876861737fbd952115960fd27fc6026e534c37117fce6f92421fe0d47fd6dc8ce3a5ff7c7fdf495e39736e277fe7b606451cf0b27ff02285083733bb7ff88eda8456cca78000fb06bb10399b80096709adf7e1858011d2e35ea2141d801a3e93cea309e28022aa1aff8ee421802b1578f2f9acf7803380adaa77574c803bebb9279bbedc8044569b6bfaa837804cc1547927c0c180552be450b69eb3805d964af43ac1228066008865478ffb806e6a9ca5705c088076d487b6485ef0807f3e499962bb398087a7e250527c4b80901151dcaa967380987a983ffde6e180a0e3b57bdf33ab80a94ca991e12bd180b1b5748396673d80ba1e16529166c480c2868f0064942880caeede8ea2421f80d35704fedcac4980dbbf0252a5f74080e426d68b90308d80ec8e81ab2d4eb580f4f603b30f313180fd5d5ca4c7a0768105c48c81e84df4810e2b934c02d4188116927104a8b651811ef925ad6b610d81275fb147dc29bc812fc613d58c4ed481382c4d580cf7d28140925dd0ef353a8148f84541c4009a81515e03ac1c3c8c8159c3991188b4b481622905739a1dcb816a8e48d3e115978172f36333ee22f2817b58549551b5ca8183bd1cf99c2723818c21bc625db91781948632d12696dc819cea804786d4c581a54ea4c70e703e81adb2a0514d4fd481b61672e7d3433381be7a1c8c30032c81c6dd9d3ff331b381cf40f504ac59e181d7a423dbeaeff781e00729c73e515f81e86a06c835c4af81f0ccbae06079a881f92f46114d893c820191a85c8bf58a8209f3e1c3aaa9e7821255f248387ad8821ab7d9ebc4261a82231998afdc52a0822b7b2e960f90968233dc9b9fec5964823c3ddfcf010fab82449efb24dbff50824cffeda30b5d70825560b74b1d4871825dc1581e9fc7f8826621d01f20ccf1826e821f4e2e318e8276e245ad55b949827f42433e2510e88287a2180229ce7b829001c3faf17161829861472a09624882a0c0a190fef33082a91fd3315f5f6a82b17edc0cb7cb9e82b9ddbc249545c782c23c737a84c53c82ca9b0210132aad82d2f967e6cd402282db57a5003fb90582e3b5b95df7321b82ec13a50180318c82f47167ec6726e282fccf0220386b0a83052c739e80405a830d89bc68cad28b8315e6dc80a436c3831e43d3e7986b918326a0a29f3358f2832efd48a900d050833759c6068c8c86833fb61ab96231e183481246c30d4e2183506e4a2519587c8358ca24e111b19f836125d6f881a3af836981606cf4624b8371dcc13ff50a91837a37f9730ea3188382930907cc1bfb838aedefffb84ed3839348ae5c5dfebf839ba3441f47d86283a3fdb14a0071e383ac57f5de124af283b4b211dd07cccb83bd0c05486b4a3583c565d021c6ff8083cdbf726aa5129083d618ec248f92d883de723d5110795d83e6cb65f1b1a8b683ef246607fced1583f77d3d957bfc3d83ffd5ec9bb8758f84082e731c3be205841086d1188fb4358418df06923d4855842137138acde43984298ef803cab7588431e6b3febcdacb843a3e477d2d5150844295b280a5074e844aecf50aacd2d18453440f1ccd7393845b9b00b88f92f58463f1c9df7bc40b846c486a931a839284749ee2d4f437ff847cf532a691317284854b5a0979a9c5848da158ff35c4868495f72f894d8ef8849e4cdda949001d84a6a26360aff8ad84aef7c0b10a431f84b74cf59bdf93a784bfa20222b7883984c7f6e64719a88f84d04ba20a8d661f84d8a0356e9a1c2b84e0f4a074c70fb684e948e31e9b6f9284f19cfd6d9e545384f9f0ef6356c062850244b9014b9fed850a985a4903c8f88512ebd33c05fb52851b3f23dbd8e0a18523924c2a030c5e852be54c280afbd585343823d7771630853c8ad339cdac6c8544dd5a5094f963854d2fb91d5321cf855581efa18e3440855dd3fddecc2930856625e3d692e2f2856e77a18a682dc38576c936fbd1bfc1857f1aa42c5538f285876be91d782344858fbd05d0bff28d85980dfa47b2049485a05ec683d3a10785a8af6a86a9f98885b0ffe651ba29a885b95039e68936e885c1a065469c10c085c9f0687377909d85d240436ea079e285da8ff6399b79ed85e2df80d5ed281585eb2ee3451a05ad85f37e1d88a67e0985fbcd2fa216e67986041c1992ef7e51860c6adb5cb46ee58614b97500e9cb91861d07e6811391b58625562fdeb5a8ba862da4511b53e2128635f24a3871f93b863e401b379393bf86468dc41a3c4137864edb44e1ef7b4d8657289d9030a5ba865f75ce26830e508667c2d6a669ecf086700fb71168639586785c6f69017e538680a8ffaeb833588688f567e40f62ed869141a80a89d77786998dc023aa457d86a1d9b030f34ba686aa257833e772ba86b271182e092da686babc9020dad97c86c307e00ddebd7786cb5307f6970afa86d39e07dc85dd9386dbe8dfc12d3afa86e4338fa60f131986ec7e178cad400686f4c8777689860c86fd12af652593a387055cbf5a03017e870da6a756a352818715f0675c87f3cc871e39ff6d323cb58726836f8a236ece872eccb7b4dcb5e9873715d7eedf2810873f5ed039abc5938747a7a096c37901874ff04907a7172a875838c98dd75f28876081222ad4fa588768c952e0207c608771115baf3a63308779593c99a317038781a0f5a0daea628789e886c6621a2387922ff00bb8cd70879a7731725f15be87a2be4afbd4eedd87ab053ca99a3eef87b34c067d2ed66d87bb92a87812702887c3d9229bc4b14d87cc1f74e9c5296187d4659f6393524c87dcaba20aae904f87e4f17ce096321087ed372fe6c9709587f57cbb1ec76f4a87fdc21e8a0f3bfe8806075a2a1fcee8880e4c6e00780aa88816915a0e96bc4a881ed61e55fa9b4488271abad8224978882f5f2f968c533b8837a37c92b72f51883fe7a1ce213ef088482b9f4a48cdc388506f7508ac11ea8858b3230ac92bfe8860f6a9521e270e88693a07e028f8a888717d3eb66780d18879c04dd6578a0f888203354176c967888a45f4f942de5f8892888cff3952fe889acafd54d79bd388a30d45fb9b17ef88ab4f66f50110ec88b391604286baee88bbd331e5a934a188c414dbdfe5874288cc565e32b8a69788d497b8df9f70f988dcd8ebe816af4f88e519f74d9b151688ed5adb11a9405e88f59b9735bdb9cc88fddc2bbb54f49e89061c98a3eb4eab890e5cddf0fd106389169cfba4066cd5891edcf1be8381ae89271cc041f05737892f5c672fc8e05d89379be68988fab2893fdb3e50ac6e6789481a6e86aeee57895059772d0c180389589858453f73978960d711d0c473e6896915a3d11676748971540e47b0c37189799251360e8dbb8981d06c9daaf2e3898a0e608000fb2e89924c2cde8b9992899a89d1bac5abbe89a2c74f1629fa1789ab04a4f23337bc89b341d3505c028689bb7eda321ee30a89c3bbb998f64c9d89cbf871865c9d5289d43501fbcc1dfd89dc716afabf023589e4adac84af685689ece9c69b17598089f525b93f70c99e89fd61847335975f8a059d2837df8c438a0dd8a48ee85c908a1613f979c9a75d8a1e4f26f9fcf6928a268a2d10fbbee58a2ec50bc03f5fdf8a36ffc3094123df8a3f3a52ed7a40188a4774bb6e63d4948a4faefc8d76ec398a57e9164c2c7cc48a602308abfd66d18a685cd3ae6275d68a70967754d4602c8a78cff3a0cbc70c8a81094893c1368f8a8942762f2d25b58a917b7c7487f6618a99b45b6549f55f8aa1ed1302eb5a648aaa25a34ee4480d8ab25e0c4aaccbe68aba964df7bcde668ac2ce68578c62f38acb065b6b9327e58ad33e273548e6868adb75cbb62543138ae3ad48ef9fccbe8aebe49ee32ffdb28af41bcd924d3b0f8afc52d4fe6ed4f28b0489b5290c06748b0cc06e139bf5a78b14f6ffbf95b3a28b1d2d6a2e703c758b2563ad61a277378b2d99c95aa336038b35cfbe1ae935f58b3e058ba3eb1f338b463b31f71f84e88b4e70b115fce54d8b56a60901f9a9a28b5edb39bc8c26368b671043472a9a668b6f4525a34b309f8b7779e0d263fe608b7fae74d5eb04398b87e2e1af562dd28b901727601b51e78b984b45e9b0324b8ba07f3d4d8a7bed8ba8b30d8d1fc6d48bb0e6b6a9e596268bb91a38a55158258bc14d9380d866338bc980c73df004d48bd1b3d3de0d63ad8bd9e6b962a59d898be21977cd2db8598bea4c0f1f1aa5338bf27e7f59e140578bfab0c87ef651318c02e2ea8fce8a578c0b14e58dde898c8c1346b97a9ad7c48c1b78665777e9238c23a9ec25ea1d008c2bdb4ae765bde38c340c829d5f018c8c3c3d93494a08f18c446e7cec9ae03f8c4c9f3f88c57ee08c54cfdb1f3dc7768c5d004fb17787e28c65309d40e679448c6d60c3cefe3ffb8c7590c35d326ba78c7dc09becf6772d8c85f04d7fbdc8b68c8e1fd816fbb1b08c964f3bb4236ed28c9e7e7858a8281f8ca6ad8e05fcf0df8caedc7cbd94c7ac8cb70b4480e296698cbf39e55159324d8cc7685f306b5bdf8ccf96b21f8bbef68cd7c4de202cf2bf8cdff2e333c179be8ce820c15bbbc1cc8cf04e78998e241b8cf87c08eeaae5398d00a9725c84350e8d08d6b4e48c2ee08d1103d08834d9538d1930c548f0266f8d215d93282ff3988d298a3a2766099c8d31b6ba48041ca78d39e3138b7bcc528d420f45f33ea39b8d4a3b5180be18e98d526736356b8e108d5a92f412b8504e8d62be8b1a1598538d6ae9fb4cf48a3c8d731544acc635978d7b40673afb95678d836b62f90590218d8b9637e854f7b18d93c0e60a5a89798d9beb6d6086ee578da415cdec4abaa08dac4007af166e268db46a1aaa5a743a8dbc9406df8723aa8dc4bdcc500cbec38dcce76afd5b735b8dd510e2e8e35ac28ddd3a34141479d38de5635e805ec0f08ded8c622f320bff8df5b53f21fe22748dfdddf55a32b74a8e060684d93f690c8e0e2eeda093c1d18e16572fb19f37418e1e7f4b0dd12a958e26a73fb698e8978e2ecf0dad65a9a88e36f6b4f3a691bd8e3f1e358acab0628e47458f744100bd8e4f6cc2b178698c8e5793cf43dfbd2b8e5fbab52ce5b9948e67e1746df9085c8e70080d08883ebc8e782e7efe01dd8d8e8054ca4fd4514d8e887aeeff6df21d8e90a0ed0e3d03c38e98c6c47dafb5b28ea0ec754f3423008ea911ff843852748eb137631e2a367c8eb95ca01e77ad388ec181b6868e80748ec9a6a657dc65b08ed1cb6f93cefe1b8ed9f0123bd3d69b8ee2148e515867cb8eea38e3d5ca15fa8ef25d12ca9631338efa811b3129f5378f02a4fd0af289888f0ac8b8595d015e8f12ec4d1dd65bb78f1b0fbb59cb83498f2333030ea94e918f2b56243ddc7fcc8f33791ee8d1c4fe8f3b9bf310f5b7ee8f43bea0b7b4de2d8f4be127de7ba9128f54038886b675c18f5c25c2b1d18d298f6447d6613924058f6c69c396595ae08f748b8a529e3e168f7cad2a9773c5d48f84cea46645d61b8f8ceff7c0803ebe8f951124a78ebb6b8f9d322b1cdcf3a38fa5530b21d67ac58fad73c4b7e6d0078fb59457e0795e7d8fbdb4c49cf97d188fc5d50aeed26ea88fcdf52ad76f61e08fd61524583b71518fde34f772a1a3738fe654a4280ceaa58fee742a79e825268ff6938a699e1d248ffeb2c3f89988b29006d1d7284509d1900ef0c3fa0b2e6c90170f8a6f56705f901f2e2a8991357490274ca44a25cf65902f6af7b27e7be090378924c4056487903fa72b80249eef9047c50be8462caa904fe2c5fdd3fb3b90580059c237e42390601dc736dbacdf90683b0e5d2906e79070582f36898fb190787529c466d0b3908091fe082a3f699088aeac033d3d499090cb33b70917d49098e79524f7089090a103d04e70350690a91fe534ddaecc90b13bd3d9a8738290b9579c3e396cd090c1733e63f9706e90c98eba4c51402290d1aa0ff8a989c290d9c53f6a6ae73590e1e048a2fdde7490e9fb2ba3cae19190f215e86e3a4ead90fa307f03b4700691024aef65a17bf0910a6539956994da91127f5d9474c94d911a995b642b13f49122b33305f45b93912acce47b3873139132e66fc55f197a913affd4e5cff9f491431913ddf2abd2914b322caf2eb28a91534b1f5aeb7db7915b63ebe290692091637c924784bcb7916b95128b2fac969173ad6caef8590a917bc5a0b445ce889183ddae9c7f05bc918bf596690ae37d91940d581b5038da919c24f3b4b5c31691a43c6936a22ba691ac53b8a27c083c91b46ae1f9a9dac091bc81e53d92115491c498c26f9b065691ccaf79912b006591d4c60aa3a8325991dcdc75a878bb4d91e4f2baa102a69e91ed08d98eabebec91f51ed272da6f1c91fd34a54ef4005592054a52245e5c0d920d5fd8f47f2afb92157539c0bc0226921d8a748a7a62de92259f89531fbac1922db4781c1163bb9235c940e6b4a40a923ddde3b46eae3d9245f26086a4a134924e06b75ebb882492561ae83e185a9c925e2ef3261ffc7c926642d818373e03926e569715c2dbc592766a3020277eb4927e7da338c9bc21928690f0610e15b8928ea4179a58f9889296b718e60ec1ff929ec9f44593b5ef92a6dca9ba4c089192aeef39459bd97d92b701a2e8e734ba92bf13e6a59212b292c726047d00583c92cf37fc7095d69a92d749ce81b64b7a92df5b7ab1c560fa92e76d010226ada692ef7e61743db47c92f78f9c096de4ee92ffa0b0c31a9ae09307b19fa2a71ead930fc268a976a5279317d30bd8ec4f96931fe389326b2bc29327f3e0b75633e59330041269104ec09338141e48fc4f8a93402404587cf5fc934833c498f4ee539350435f0bc6d14c935852d3b2552427936062228e0258a99368714ba030cd229370804eea42cc6693788f2c6d9a8dd493809de42b9a35549388ac7625a3d35f9390bae25d1964f69398c928d35cd3b093a0d74989cff5ae93a8e54481d48da793b0f319bccc4ae693b900c93c18c94993c10e53011b914493c91bb70d3617e593d128f561c9bed293d9360e0037d44c93e14300e9e1932f93e94fce202822f793f15c75a46c97bb93f968f7780ff237940175539c731fc59409818a12f6fa6494118d9adcfc48b694199985fbe3be069421a54b710dfa429429b0eb3ddb8a059431bc6563ace6939439c7b9e3e275da9441d2e8bfdc8a779449ddf1f8fb63b49451e8d5909f2d8c9459f393882800ad9461fe2be0f5e271946a089e9c68c4ef947212ebbbe086ed947a1d1340bcf3eb948227152c5dc421948a30f180229c8094923aa83d6b0eb4949a44396596992794a24da4fa04a6ff94aa56eafc14902494b2600b6d25993c94ba69064e96f3b194c271dba1c7bdb094ca7a8b6817022c94d28315a2e3b8dc94da8b7a538cc64194e293b97b70fba494ea9bd31bef171a94f2a3c73665c38494faab95cc33988e9502b33edeb71ab5950abac26f4ebb469512c2207f58d860951ac9591033bcf29522d06c233da0c5952ad759b9d4a8729532de21d556e56e953ae4c4772256039542eb41a094e557954af199530c6b6a9552f7cb8fe6ad1d955afdd858815c28956303bfae3a1728956b0981926e699b95730f1e067bcbdf957b14950bbfa336958319e6a39741c9958b1f12cf5fe6a6959324199076bdc3959b28fae838e00295a32db6d803532b95ab324d613309f695b336be8524e40795bb3b0a4535adf395c33f30a2c2213b95cb43319f26e45595d3470d3bc08aa995db4ac379eb949595e34e545b046f6b95eb51bfe067757595f355060b70edf595fb5826dd7d0d2896035b2257e7f446960b5df87c0db183961360a94b4a4014961b6334c6f9882a9623659af0775ef9962b67dbc91f86b6963369f7524dae9a963b6bed8d5d72e596436dbe7baa5cdd964b6f6a1e8fe2cc965370f07769680a965b725187923cf79663738d50659f02966b74a3d33eb8a1967375951178a161967b76610c6e5dd896837707c57adfb3968b77893df905ac969377e577439b98969b781c72b55a5e96a3782e31a8e7fa96ab781ab578d78596b377e1ff7fa92e96bb77841117ca4196c37700eb9b952696cb76589065516496d3758b00cf339f96db74983e335d9e96e3738049ebde4796eb72432552b1a796f370e0d1c1c0ee96fb6f595092e27097036daca31fd9ae970b6bdacac2574b971369e3c8d3f91a971b67c79eae4a17972365864daac269972b631fd722c769973360943c6faba0973b5de37eeaaec397435b0d9fecfdbe974b5812a0cfb2b0975354f282ebd4ec975b51ad479a58fc97634e42f03420a2976b4ab37e11fadb977346fef28ca3da977b43254efcc51297833f2694baf532978b3b02c51fb829979336b9e1837f23979b324beb3ea89197a32db8e3a9802497ab2900cc1c3ed397b32423a5ef0ad897bb1f217279f7b897c319fa3315063b97cb14ade918247897d30f3c95db2dcc97db09a63ab5eae397e303ead90011b897eafe0a7211459197f2f8050741170697faf1da99e704049802eb8b2b5a77c6980ae516bcf2cade9812de7d50074334981ad7bee5ef14069822d0db80015deb982ac9d31f952ed39832c2a5c601820c983abb53749d403d9842b3dc2cbf3f6d984aac3fefbe43009852a47ebef0fbbf985a9c989bae07cf9862948d874bf2bd986a8c5d8321357a9872840890843659987a7b8eb0cb491a988272efe54caee2988a6a2c2f5e96409892614390571b30989a5836098c471a98a24f039c5410d498aa45ac4a045ca398b23c3013f2fc3c98ba328efb75aeca98c228c901e220e698ca1ede288deca298d214ce70ce998398da0a99dbf99c8898e200406b64582798e9f5c220641c5198f1eb1efc4e267298f9e0570077a1739901d56a2e35a5bb9909ca5886dd39319911bf220bc34f3c9919b3c6be3cc8c69921a8469f9e743d99299ca1b13d0d91993190d7f46d3e3b993984e96a839d3b994178d614d4af1899496c9df4b4e5e6995160410b78a142995953bf5a742e5799614718e2fbc7df99693a4da663962399712d5da5ffaefb99792048e32415d29981130f5f24bbaa998905b11b557f139990f82e190a2c3a9998ea8659967ce099a0dcb9de4e185e99a8cec8a88493aa99b0c0b2b98d715499b8b27812bc218799c0a418b564021099c89594a2d85e5899d086ebdc6c6f6d99d8781e63735bf899e0692c3940384b99e85a155f26065e99f04ad9d677b5ca99f83b79a08823d29a002bf4beaa1b649a081c4b323055149a100c7cfc6d77239a17fc8a1eb415809a1fec729a56b1c69a27dc3670a7bb409a2fcbd5a2f98eeb9a37bb50329e77759a3faaa620e8ad3e9a4799d76f2a565c9a4f88e41eb5869c9a5777cc30dc3f809a5f668fa6f070449a67552e8243f5de9a6f43a8c4289afe9a7731fe6df018119a7f202f80ec13459a870e3bfe6e20819a8efc23e7c7c1719a96e9e73e4a65839a9ed786034769e59aa6c5003810198b9aaeb255ddf5ad319ab69f86f6494b569abe8c93825c08459ac6797b837ee6109ace663efb02d4959ad652ddea38b1809ade3f58527148479ae62bae34fd52329aee17df932d76589af603ec6e5249a19afdefd4c7bc4ec99b05db98a0bbf6619b0dc737faa19ecc9b15b2b2d6bd94489b1d9e09366010e99b25893b1ad93c9b9b2d744885792d279b355f31778fe6329b3d49f5f26d593b9b453495f76165a69b4d1f1187bbd8af9b550968a4cc6d7a9b5cf39b4fe2cd0b9b64dda98a4e8e489b6cc793555f35ff9b74b158b26436e29b7c9af9a2acf18e9b8484762788b4859b8c6dce4246bc369b945701f43632fc9b9c40113ea6311c9ba428fc22e5bcce9bac11c2a243ca359bb3fa64be0f3b659bbbe2e27796e0699bc3cb3bd029773a9bcbb370c915abc99bd39b8163aa17fc9bdb836da13543b09be36b358305a4ba9beb52d90a699eea9bf33a5838af840c9bfb21b30f2593e89c0308e98f19fc429c0aeffbb9dad8de9c12d6e990b633839c1abdb314fa03f59c22a45847f42fff9c2a8ad92af28b6f9c327135bf42d8189c3a576e0632c5d29c423d82010ff2839c4a2371b127ea149c52093d17c8267d9c59eee4363e0fbf9c61d4670dd6fbec9c69b9c59fe02f1f9c719effeda6db8a9c798415f87821699c816907c1a10f0f9c894dd54a6ea0e49c91327e942dc15e9c991703a02b49119ca0fb646fb3fea69ca8dfa1041496db9cb0c3b95e99b48f9cb8a7ad808fe8b69cc08b7d6b43b2669cc86f2920017ecf9cd052b0a015a9439cd83613eccc7b339ce0195307722c339ce7fc6df152e1f89cefdf64abbab05f9cf7c23737f599679cffa4e5974f8d399d07876fcb146a239d0f69d5d48ffca09d174c17b50dff539d1f2e356dda1b0d9d27102f003fe6cb9d2ef2046d8ae7bb9d36d3b5b70691369d3eb542ddfe44cc9d4696abe3bd523c9d4e77f0c98ef77b9d56591190be60af9d5e3a0e3a96a8389d661ae6c862d6ad9d6dfb9b3b6de2dd9d75dc2b9502b1d39d7dbc97d66c16d09d859ce000f4d3599d8d7d0415e7972d9d955d04168f00489d9d3ce004359aec9da51c97e025e1999dacfc2babaa3d129db4db9b680d04609dbcbae716987cd09dc49a0eb896d9f89dcc79124f523db29dd457f1dc14b8269ddc36ad602847c49de41544dcd6d9489debf3b8536a47be9df3d207c52c5c7a9dfbb0333366cf269e038e3a9f6345bb9e0b6c1e0a6b54839e1349dd75c87e1d9e1b2778e2c4337b9e2304f052a7d3e79e2ae243c6bcad029e32bf73404bfac49e3a9c7ec09ee77f9e42796648fe8be29e4a5629dab3eef79e5232c9770806259e5a0f451f43b5329e61eb9cd4afce479e69c7d0989511e99e71a3e06c3c2f049e797fcc50edc2e79e815b9447f259449e89373852926c369e9112b87216643e9e98ee14a7c698479ea0c94cf4eb4da69ea8a4615accb81a9eb07f51dab2f9cf9eb85a1e75e623629ec034c72dae33dc9ec80f4c035318b79ecfe9acf81cadde9ed7c3ea0d52bdb19edf9e03443d01039ee777f89e231f1c9eef51ca1c4cadbb9ef72b77c00131199eff05018a881be49f06de677d28cf489f0eb7a9992a9aeb9f1690c7dfd4bcf19f1e69c2526e61fb9f264298f23ea52b9f2e1b4bc08c90219f35f3dabe9f1b039f3dcc45edbd2c769f45a48d4f2d99a79f4d7cb0e43726459f5554b0ae20848a9f5d2c8cae3055349f650444e5ad278f9f6cdbd955dd796e9f74b34a0007b7309f7c8a96e5723bc49f8461c0076350a49f8c38c567212ddd9f940fa705f1fa0a9f9be664e51bca5a9fa3bcff05e4a28e9fab9375699274fd9fb369c8116b22929fbb3ff6feb47ad29fc3160232b43bd69fcaebe9aeb012569fd2c1ad73ed99a09fda974d83b25ba09fe26cc9df43d0e19fea422287e760889ff217577ee2605e9ff9ec68c57a14cca001c1565cf3b0dca00996204694563ca0116ac683a11540a0193f49155eecdfa02113a7fd12cabba028e7e33c018b1ca030bbfad36ff8f3a0388feec4a2cddea04063bf10deb227a048376bb9683cc2a0500af4bf83f355a057de5a24764a35a05fb19be983a468a06784ba0ff053a7a06f57b49900985ba0772a8b85f8a1a6a07efd3ed81c8d5fa086cfce90b06813a08ea23ab0f82d07a09674833a37c63aa09e46a82db30c6aa0a618a98cadc70aa0adea87586bac4fa0b5bc419230612aa0bd8dd83b3f7950a0c55f4b54dc7733a0cd309ae04acc07a0d501c6decdd7c8a0dcd2cf51a8e931a0e4a3b43a1f3dc6a0ec7475997401d1a0f4451370ea5066a0fc158dc1c53363a103e5e48d47a36da10bb617d4b487f8a1138627994eb745a11b5613dc58f663a12325dc9f15f930a12af581e2c8625ea132c503a8b2c36ca13a9461f2179cb0a142639cc0395d55a14a32b4145a6359a15201a7efbcfb93a159d07853a361b3a1619f25414fc03fa1696daeba04309ba1713c14bf02bb06a1790a57518d569ca180d87672e5e955a188a672244e480da190744a6708367ea19841ff3c556743a1a00f90a5777bdca1a7dcfea3b004aea1afaa4938408102a1b77770646a5f06a1bf4474296efbd4a1c71154888fa36fa1cede11830d90c0a1d6aaab1a29eda0a1de77214f25d2d5a1e643742342480ea1ee0fa397c043efa1f5dbafade0ac0aa1fda79866e454e3a205735dc40c01f0a20d3effc698659da2150a7e6fca214aa21cd5d9c0e1c54ea224a111bb1fd0f8a22c6c265fc4b28ea2343717b010c753a23c01e5ad445b83a243cc90589faa58a24b9717b362de0aa253617bbece0fcfa25b2bbc7c2147dda262f5d9ec9c7d6ca26abfd4117f96b8a27289aaec0a68fda27a535e7d7cb87fa2821ceec7163887a289e65bca168b68a291afa587bd4277a29978cc0149de1da2a141cf37fbcdc3a2a90aaf2d126fe7a2b0d36be1cd120da2b89c05576af0d0a2c0647b8f2b37d2a2c82cce8a4d01cda2cff4fe4a0f5889a2d7bd0acfb134e2a2df84f41c717ecca2e74cba318f0d4aa2ef145d1048a67ea2f6dbdcb9dcff9ca2fea3392f8abcf3a3066a72729071efa30e3188842ca115a315f87b659dbc08a31dbf4b18222389a32585f79cf82777a32d4c80f55e06d5a33512e72291efc4a33cd92a25d1ff89a3449f4a005c428fa34c6546b36eb465a3542b2040473fbfa35bf0d6a823be7ba363b669ec41f99ea36b7bda0ddfa959a37341270e3a7505a37b0650ee8ff32da382cb57b01da984a38a903b54210ceea39254fbdbd78180a39a1999487e5a7ea3a1de139b52da61a3a9a26ad59232d2a3b1669ef87984b3a3b92ab00545e01aa3c0ee9dfd344452a3c8b268e1819fe1a3d07610b36ad087a3d83995742ca33ca3dffcf72503d435a3e7c035c72d0ee5a3ef83515be4edfca3f74649e467fb6aa3ff091f61f2b060a406cbd1d5c17550a40e8e614110a1efa41650cda51c7d35a41e131703213d62a425d53d5c5b07fba42d9740b205f1cda4355921055dfeeca43d1ade579f22baa444dc78aa053fdea44c9deffdcc2854a4545f44542f9d5ca45c2075ae6b4f8da463e1840dbadec9a46ba26f7359da44a4736337e083c083a47b23dd5673ff65a482e45fd665f414a48aa4bf6194eb16a49264fbf93c2048a49a25159e96bedba4a1e50c52dfe15ea4a9a4e0175291b7a4b16490ed29c929a4b9241ed5a07056a4c0e389d1f15f38a4c8a2d1e3575d2fa4d061f70b0d20f7a4d820f94a4d50b0a4dfdfd8a25281daa4e79e951457395aa4ef5d2ea195eb7da4f71ba54b48fbf1a4fed9f912aabdcea5069829f8f57396a50e5637ff634f31a5161423272e71f7a51dd1eb7190eca5a5258f90dfc4bf6da52d4d137303d9e8a5350a732c881b25a53cc7b00d8b519ea54484ca17473b44a54c41c14af58577a553fe95a9cfcd0ea55bbb47350f9e53a56377d5edee7507a56b3441d5a5bc65a572f08aed6ecf1ea57aacb13682f75ea58268b4b21b6ecda58a249561715e8ca591e05345bddf40a5999bee6039f907a5a15766b21ea37fa5a912bc3ca4c5caa5b0cdef0105368aa5b888ff0078bbe5a5c043ec3c380b84a5c7feb6b57bca98a5cfb95e6d7c8dd8a5d773e36572d983a5df2e459e97215ea5e6e8851a21c8bca5eea2a1d94b227aa5f65c9bdd4b7102a5fe1673275ae64ca605d027b8b1a3dda60d89b99287bacca6154328b6152bbfa61cfc752491e6f1a624b59edf35cc2fa62c6ea5e738aad9a634278a3dd241e9a63be04be43a3feca64398eadba84308a64b51672553d8fda65309c0c2747f25a65ac1f7b441a276a6627a0bfbf29f82a66a31fd9abec278a671e9cc91dd4729a679a178e2855903a68159028dee1315a6891069954e8014a690c7adf9dd9a56a6987ecfbcd24bd4a6a035cedf636e32a6a7ecab62c7cab5a6afa36548361a4fa6b759fc90e50598a6bf10713e0b24d5a6c6c6c350defff4a6ce7cf2ca970e94a6d632ffac69b7fca6dde8e9f78d5324a6e59eb1ad3826b9a6ed5456cea06913a6f509d95cfc403fa6fcbf395981c1ffa7047476c566f3c7a70c2991a1e1cac1a713de89f0282bd1a71b935fb16feb90a7234812e6eece53a72afca391da8826a732b111b368bcd5a73a655d4cceffe5a74219865f42d499a749cd8cebf9adf5a7518170f428eebea75935327905e976a760e8d17bc5e064a7689c4dfd9e0595a7704fa7ffc37ad6a77802df836b51bca77fb5f489ca8ba3a78768e7141619b0a78f1bb72382dccfa796ce64b945a5b9a79e80efd69334efa7a633587ca03ac2a7ade59eaca15750a7b597c267cb1a83a7bd49c3af520419a7c4fba2846a83a0a7ccad5ee848f877a7d45ef8dc21b1d1a7dc10706128eeb5a7e3c1c57892de03a7eb72f823939e6ca7f32408635f3e7ea7fad4f63929bc9ea80285c1a627070ba80a366aab8afbe0a811e6f14a896915a819975584560c7da82147975a2493cda828f7b6cd289c97a830a7b3de95b450a838578e8f9f584fa8400746e178f5cca847b6dcd555e9e7a84f66506c6981a1a85715a1a7e6f9e6a85ec4d089017f86a86673dd10ec2f3ca86e22c740da15aea875d18f19fe2f69a87d80349d8b68eca8852eb7ccb49e9da88cdd18a8ac9cd5a8948b5732a61fdba89c39736bd3d3e7a8a3e76d55685522a8ab9544f0962faaa8b342fa3e8fdf8ea8baf08d4087d0d5a8c29dfdf7b05f7ba8ca4b4c653bd772a8d1f8788a5c74a8a8d9a58268446302a8e1526a0025be61a8e8ff2f533292a0a8f0abd2629cdb9ba8f858532f968527a90004b1bb516b1ba907b0ee06ff594fa90f5d0813d20b9ca91708ffe2fb2ddda91eb4d575ac5bf1a9266088cd1721baa92e0c19ea6cfb23a935b788cedf541ea93d62d57b9f88a1a9450dfff1dee4b0a94cb90832cea458a95463ee3f9ff3afa95c0eb21983eedca963b953c1aba20fa96b63d33948098ba9730e30818a119fa97ab86b9ba296b0a982628488c26530a98a0c7b4a1a39a8a991b64fe0dac0b2a99960024e349703a9a1099293584960a9a8b300b17654aaa9b05c4ca9bf25d9a9b805767d631a00a9bfae7e2d927e4ca9c75763bb7d9009a9cf002728547c9aa9d6a8c875476187a9de5147a3864c75a9e5f9a4b4413b28a9eda1dfa8a81b86a9f549f881eacb9ba9fcf1ef41391994aa0499c3e7c2c3c3aa0c417676b778a1aa13e906ef46d6cdaa1b907552a06d10aa2337c1a1f3ba5baa2adeebde702dccaa3285f4094526a7aa3a2cda23a1f463aa41d39e2eb5d6a1aa497a402baffd32aa5120c01bbf8818aa58c71e00138784aa606d59d9dafbdaaa681373aa44d5b2aa6fb96b727ff5d7aa775f4133bb2d4caa7f04f4ef253d49aa86aa86a5ecd73caa8e4ff659409cceaa95f5440a4f1fe2aa9d9a6fba46e295aaa53f796a565740aaace4611babe079aab48926cf75d117aabc2dca86e26c2baac3d24c431fe50caacb76ac055c5f4faad31ae9cec5eecdaadabf05a08a97a3aae262ff7bd84e31aaea06d761dcf71faaf1aa8d53c6675baaf94e2152c2641cab00f1935ffea2e1ab0894e37ca8c973ab103811a9ee6de7ab17db1de8fd16a0ab1f7e083b023a4dab2720d0a12b3fe9ab2ec3771ca57ec6ab3665fbae9e3e7eab3e085e5842b705ab45aa9f1ac0109cab4d4cbdf74363daab54eebaeef9b9abab5c909603100b54ab64324f34b3426eab6bd3e6851038ecab73755bf553b918ab7b16af86aa7d9aab82b7e13a413174ab8a58f111447004ab91f9df0ce0c50aab999aab2e42ac9faba13b557696933faba8dbdde708d5c8abb07c4480c5c178abb81c8944f993f3abbfbcac34d07b3eabc75cad517695c7abcefc8c9c17f25fabd69c4a15e09040abde3be5bffc5f10abe5db5f9b973ed8abed7ab7a9dd0011abf519edebf9639eabfcb90263181ad1ac0457f51064c768ac0bf6c5f50afb90ac139575123639e6ac1b34026911f57bac22d26dfac991d0ac2a70b7c88862daac320edfd379ad01ac39ace61cc8a528ac414acaa5a070a2ac48e88d6f2c253cac50862e7a96c93fac5823adc90b536bac5fc10b5bb4aaf9ac675e4733bda7a3ac6efb615251119cac769859b899a199ac7e353067c200caac85d1e560f4c8e3ac8d6e78a55c8416ac950aea3623ad1aac9ca73a1474af2aaca443684179e604acabdf74be5d9decacb37b5f8c4a13aeacbb1728ac69749dacc2b2d01fe5de94acca4e55e7e95ff9acd1e9ba059df7bdacd984fc7a2d955eace1201d46c218e5ace8bb1c6c8552ecacf055f9eca10499acf7f0b5c83edfa7acff8b500088865ead0725c896a78b99ad0ec01f8bc572caad165a54e10baff5ad1df46897a3a7b5ad258e5ab0b6af3aad2d282b2d6e0c4ead34c1da0ef2f551ad3c5b67566e9141ad43f4d30509f7b4ad4b8e1d1bee30dead5327459c44358ead5ac04c8734ef33ad625931dde937dbad69f1f5a189da35ad718a97d33f918ead792318743309dbad80bb77858cdfb0ad8853b50875a048ad8febd0fe15c982ad9783cb6795c9e6ad9f1ba4461e00a3ada6b35b9ad6bd8eadae4af166e84129adb5e265ab7abca1adbd79b869b651ceadc510e9a2c31335adcca7f957c9040badd43ee789f01830addbd5b43a603438ade36c5f6a412d68adeb02e91abac9b6adf299514cf4bfccadfa2f980216b709ae01c5bd3b484780ae095bc0f9b0f9fcae10f1a33e784800ae1887640ac59bc7ae201d035fc05043ae27b2813e8fb127ae2f47dda85afaddae36dd189e495a8cae3e72322181ee1cae46072a332bc430ae4d9c00d46ddc31ae5530b6066f2643ae5cc549ca56834dae6459bc214ac4feae6bee0d0c72adc2ae73823c8cf4f0d3ae7b164aa3f8322aae82aa3752a3068aae8a3e029a1bf380ae91d1ac7b896f5fae996534f811e148aea0f89c10dba126aea88be1c70cf7b1aeb01f061bcc1e6eaeb7b209103f3fb1aebf44eaa58c76a0aec6d7aadcd9cf2daece6a49b74d4621aed5fcc7360cc914aedd8f235a3e3675aee5215e25075d86aeecb377978dfe61aef4456fb2f7c9f4aefbd746786a6207af0368fbe90b593daf0afa900600330faf128c02d06e63d5af1a1d54497b50c2af21ae84724c4fe6af293f934c06a82faf30d080d7cf916daf38614d16cc344caf3ff1f80a21aa5daf478281b2f4fe13af4f12ea126b2ac6af56a33129a91cadaf5e3356f9d3b0ebaf65c35b840fb587af6d533ec981e970af74e300cb4efc7daf7c72a18a9b8f70af840221088c33f5af8b917f46456ca6af9320bc44ebad06af9aafd805a35989afa23ed28990c792afa9cdabd1d83d71afb15c63df9df26cafb8eafab4060eb7afc079705034ab7bafc807c4b54dd2d5afcf95f7e4757fd5afd72409decf9e86afdeb1faa5800be5afe63fca39aa95ebafedcd789c72fb87aff55b05cefceca8affce871d26c0a32b00475bca7e3e60ab00c02e650880310b0138feecd7bd522b01b1cd61fe2c121b022a99c48e01ce9b02a364149972f5eb031c2c5232b3062b0394f27d6bf48dcb040db69657692b9b0486789d07418e8b04ff38918dad763b0577f673fcdbb29b05f0b24466fa244b06696c02de35bc5b06e223af74ba7cab075ad94a3cb377cb07d38cd3484ad10b084c3e4aa9a9bcbb08c4edb072f8800b093d9b04b65e711b09b646478601f72b0a2eef78f4088a8b0aa796991296b4eb0b203ba7f3d010eb0b98dea5a9d74abb0c117f9246ce1ffb0c8a1e6ddcd55f5b0d02bb387e0ce97b0d7b55f23c93b05b0df3ee9b2a87b7ab0e6c85335a06149b0ee519badd2aee7b0f5dac31c6117e2b0fd63c9826d40e7b104ecaee118bfc2b10c757339851b62b113fe168cd3cbd4b11b8698dc263a49b1230efa289dc116b12a973a735babb0b1321f59bd8136b9b139a758082f8ff1b1412f355487d647b148b6f1a3ab19ccb1503e8cf6ba5bc0b157c6074ed68e89b15f4d60ad2095bcb166d49912b94617b16e5bb080c16588b175e2a6f859ab2bb17d697c7aa2bf4bb184f03108bd3b65b18c76c4a3c9aa26b193fd374ce8876db19b8389053a404fb1a309b9cddf3313b1aa8fc9a7f7af39b1b215b894a3f573b1b99b86950437aeb1c12133aa389910b1c8a6bfd5612df5b1d02c2b179dfbf8b1d7b175720ef9eeb1df369ee5d40fe7b1e6bba7740d1732b1ee408f1dd9da5fb1f5c555e45a1539b1fd49fbc8ad74cfb204ce80cbf39772b20c52e4ef4c0cb4b213d72833d6556bb21b5b4a9ab1e3b2b222df4c24fe1aebb22a632cd3da4fbfb231e6eca865c81ab2396a8ba3bfbb36b240ee09c7075195b2487167135ba504b24ff4a389dbc09bb25777bf2ba6a0c0b25efab9f9db3326b2667d93f59856ceb26e004d1ffcdc0b000000000000000000000000000000000000000000000000"; } - /// @notice Fetches the rate for a given basis points value - /// @param bps The basis points value to get the rate for - /// @return ray The annual rate value function btor(uint256 bps) public view virtual returns (uint256 ray) { - require(bps <= MAX); - + require(bps <= MAX_BPS_IN); assembly { let offset := mul(bps, 8) // Each rate is 8 bytes let wordPos := div(offset, 32) // Which 32-byte word to read @@ -55,21 +31,11 @@ contract ConvMock { } } - /// @notice Fetches the yearly bps rate for a given per second rate - /// @param ray The per second rate to get the rate for - /// @return bps The annual rate value function rtob(uint256 ray) public pure returns (uint256 bps) { - // Convert per-second rate to per-year rate using rpow uint256 yearlyRate = _rpow(ray, 365 days); - // Subtract RAY to get the yearly rate delta and convert to basis points - // Add RAY/2 for rounding: ensures values are rounded up when >= 0.5 and down when < 0.5 return ((yearlyRate - RAY) * BPS + RAY / 2) / RAY; } - /// @notice Exponentiate `x` (RAY, 27 decimal places) to `n` () by squaring - /// @param x The base (RAY, 27 decimal places) - /// @param n The exponent (integer, 0 decimal places) - /// @return z The result function _rpow(uint256 x, uint256 n) internal pure returns (uint256 z) { assembly { switch x