From 69072d8967841ea5c2fa469ad52e55e1cb8afad4 Mon Sep 17 00:00:00 2001 From: jar-o Date: Fri, 12 Jan 2024 11:37:22 -0700 Subject: [PATCH] Scribe LST extension --- src/extensions/IRateSource.sol | 8 +++ src/extensions/ScribeLST.sol | 37 +++++++++++ src/extensions/ScribeOptimisticLST.sol | 37 +++++++++++ test/IScribeOptimisticTestLST.sol | 88 ++++++++++++++++++++++++++ test/IScribeTestLST.sol | 88 ++++++++++++++++++++++++++ test/Runner.t.sol | 18 ++++++ 6 files changed, 276 insertions(+) create mode 100644 src/extensions/IRateSource.sol create mode 100644 src/extensions/ScribeLST.sol create mode 100644 src/extensions/ScribeOptimisticLST.sol create mode 100644 test/IScribeOptimisticTestLST.sol create mode 100644 test/IScribeTestLST.sol diff --git a/src/extensions/IRateSource.sol b/src/extensions/IRateSource.sol new file mode 100644 index 0000000..7b46e3a --- /dev/null +++ b/src/extensions/IRateSource.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0; + +// https://github.com/marsfoundation/sparklend-advanced/blob/277ea9d9ad7faf330b88198c9c6de979a2fad561/src/interfaces/IRateSource.sol + +interface IRateSource { + function getAPR() external view returns (uint); +} diff --git a/src/extensions/ScribeLST.sol b/src/extensions/ScribeLST.sol new file mode 100644 index 0000000..849087f --- /dev/null +++ b/src/extensions/ScribeLST.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.16; + +import {IChronicle} from "chronicle-std/IChronicle.sol"; + +import {IRateSource} from "./IRateSource.sol"; +import {Scribe} from "../Scribe.sol"; + +/** + * @title ScribeLST + * + * @notice Schnorr based Oracle with onchain fault resolution for Liquid + * Staking Tokens. + */ +contract ScribeLST is Scribe, IRateSource { + constructor(address initialAuthed_, bytes32 wat_) + Scribe(initialAuthed_, wat_) + {} + + function getAPR() external view returns (uint) { + uint val = _pokeData.val; + require(val != 0); + return val; + } +} + +/** + * @dev Contract overwrite to deploy contract instances with specific naming. + * + * For more info, see docs/Deployment.md. + */ +contract Chronicle_BASE_QUOTE_COUNTER is ScribeLST { + // @todo ^^^^ ^^^^^ ^^^^^^^ Adjust name of Scribe instance. + constructor(address initialAuthed, bytes32 wat_) + ScribeLST(initialAuthed, wat_) + {} +} diff --git a/src/extensions/ScribeOptimisticLST.sol b/src/extensions/ScribeOptimisticLST.sol new file mode 100644 index 0000000..187b881 --- /dev/null +++ b/src/extensions/ScribeOptimisticLST.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.16; + +import {IChronicle} from "chronicle-std/IChronicle.sol"; + +import {IRateSource} from "./IRateSource.sol"; +import {ScribeOptimistic} from "../ScribeOptimistic.sol"; + +/** + * @title ScribeOptimisticLST + * + * @notice Schnorr based optimistic Oracle with onchain fault resolution for + * Liquid Staking Tokens. + */ +contract ScribeOptimisticLST is ScribeOptimistic, IRateSource { + constructor(address initialAuthed_, bytes32 wat_) + ScribeOptimistic(initialAuthed_, wat_) + {} + + function getAPR() external view returns (uint) { + uint val = _currentPokeData().val; + require(val != 0); + return val; + } +} + +/** + * @dev Contract overwrite to deploy contract instances with specific naming. + * + * For more info, see docs/Deployment.md. + */ +contract Chronicle_BASE_QUOTE_COUNTER is ScribeOptimisticLST { + // @todo ^^^^ ^^^^^ ^^^^^^^ Adjust name of Scribe instance. + constructor(address initialAuthed, bytes32 wat_) + ScribeOptimisticLST(initialAuthed, wat_) + {} +} diff --git a/test/IScribeOptimisticTestLST.sol b/test/IScribeOptimisticTestLST.sol new file mode 100644 index 0000000..c822c73 --- /dev/null +++ b/test/IScribeOptimisticTestLST.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {Test} from "forge-std/Test.sol"; + +import {IToll} from "chronicle-std/toll/IToll.sol"; + +import {IScribe} from "src/IScribe.sol"; +import {ScribeOptimisticLST} from "src/extensions/ScribeOptimisticLST.sol"; + +import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; + +import {LibFeed} from "script/libs/LibFeed.sol"; + +abstract contract IScribeOptimisticTestLST is Test { + using LibFeed for LibFeed.Feed; + using LibFeed for LibFeed.Feed[]; + + ScribeOptimisticLST private scribe; + + bytes32 internal WAT; + bytes32 internal FEED_REGISTRATION_MESSAGE; + + function setUp(address payable scribe_) internal virtual { + scribe = ScribeOptimisticLST(scribe_); + + // Cache constants. + WAT = scribe.wat(); + FEED_REGISTRATION_MESSAGE = scribe.feedRegistrationMessage(); + + // Toll address(this). + IToll(address(scribe)).kiss(address(this)); + } + + function _liftFeeds(uint8 numberFeeds) + internal + returns (LibFeed.Feed[] memory) + { + LibFeed.Feed[] memory feeds = new LibFeed.Feed[](uint(numberFeeds)); + + // Note to not start with privKey=1. This is because the sum of public + // keys would evaluate to: + // pubKeyOf(1) + pubKeyOf(2) + pubKeyOf(3) + ... + // = pubKeyOf(3) + pubKeyOf(3) + ... + // Note that pubKeyOf(3) would be doubled. Doubling is not supported by + // LibSecp256k1 as this would indicate a double-signing attack. + uint privKey = 2; + uint bloom; + uint ctr; + while (ctr != numberFeeds) { + LibFeed.Feed memory feed = LibFeed.newFeed({privKey: privKey}); + + // Check whether feed with id already created, if not create and + // lift. + if (bloom & (1 << feed.id) == 0) { + bloom |= 1 << feed.id; + + feeds[ctr++] = feed; + scribe.lift( + feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE) + ); + } + + privKey++; + } + + return feeds; + } + + function test_getAPR() public { + // An actual value, i.e. + // CFG_ETH_RPC_URLS="$ETH_RPC_URL" go1.21.1 run ./cmd/gofer data 'LIDO_LST/7DAYS' + uint val = 3592236075648471881; + + LibFeed.Feed[] memory feeds = _liftFeeds(scribe.bar()); + + IScribe.PokeData memory pokeData; + pokeData.val = uint128(val); + pokeData.age = 1; + + IScribe.SchnorrData memory schnorrData; + schnorrData = feeds.signSchnorr(scribe.constructPokeMessage(pokeData)); + + scribe.poke(pokeData, schnorrData); + assertEq(scribe.getAPR(), val); + assertEq(scribe.getAPR(), scribe.read()); + } +} diff --git a/test/IScribeTestLST.sol b/test/IScribeTestLST.sol new file mode 100644 index 0000000..c2d2e5e --- /dev/null +++ b/test/IScribeTestLST.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {Test} from "forge-std/Test.sol"; + +import {IToll} from "chronicle-std/toll/IToll.sol"; + +import {IScribe} from "src/IScribe.sol"; +import {ScribeLST} from "src/extensions/ScribeLST.sol"; + +import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; + +import {LibFeed} from "script/libs/LibFeed.sol"; + +abstract contract IScribeTestLST is Test { + using LibFeed for LibFeed.Feed; + using LibFeed for LibFeed.Feed[]; + + ScribeLST private scribe; + + bytes32 internal WAT; + bytes32 internal FEED_REGISTRATION_MESSAGE; + + function setUp(address scribe_) internal virtual { + scribe = ScribeLST(scribe_); + + // Cache constants. + WAT = scribe.wat(); + FEED_REGISTRATION_MESSAGE = scribe.feedRegistrationMessage(); + + // Toll address(this). + IToll(address(scribe)).kiss(address(this)); + } + + function _liftFeeds(uint8 numberFeeds) + internal + returns (LibFeed.Feed[] memory) + { + LibFeed.Feed[] memory feeds = new LibFeed.Feed[](uint(numberFeeds)); + + // Note to not start with privKey=1. This is because the sum of public + // keys would evaluate to: + // pubKeyOf(1) + pubKeyOf(2) + pubKeyOf(3) + ... + // = pubKeyOf(3) + pubKeyOf(3) + ... + // Note that pubKeyOf(3) would be doubled. Doubling is not supported by + // LibSecp256k1 as this would indicate a double-signing attack. + uint privKey = 2; + uint bloom; + uint ctr; + while (ctr != numberFeeds) { + LibFeed.Feed memory feed = LibFeed.newFeed({privKey: privKey}); + + // Check whether feed with id already created, if not create and + // lift. + if (bloom & (1 << feed.id) == 0) { + bloom |= 1 << feed.id; + + feeds[ctr++] = feed; + scribe.lift( + feed.pubKey, feed.signECDSA(FEED_REGISTRATION_MESSAGE) + ); + } + + privKey++; + } + + return feeds; + } + + function test_getAPR() public { + // An actual value, i.e. + // CFG_ETH_RPC_URLS="$ETH_RPC_URL" go1.21.1 run ./cmd/gofer data 'LIDO_LST/1DAY' + uint val = 3386003474307116517; + + LibFeed.Feed[] memory feeds = _liftFeeds(scribe.bar()); + + IScribe.PokeData memory pokeData; + pokeData.val = uint128(val); + pokeData.age = 1; + + IScribe.SchnorrData memory schnorrData; + schnorrData = feeds.signSchnorr(scribe.constructPokeMessage(pokeData)); + + scribe.poke(pokeData, schnorrData); + assertEq(scribe.getAPR(), val); + assertEq(scribe.getAPR(), scribe.read()); + } +} diff --git a/test/Runner.t.sol b/test/Runner.t.sol index 06d2557..518986d 100644 --- a/test/Runner.t.sol +++ b/test/Runner.t.sol @@ -5,9 +5,13 @@ pragma solidity ^0.8.16; import {Scribe} from "src/Scribe.sol"; import {ScribeInspectable} from "./inspectable/ScribeInspectable.sol"; +import {ScribeLST} from "src/extensions/ScribeLST.sol"; +import {ScribeOptimisticLST} from "src/extensions/ScribeOptimisticLST.sol"; import {IScribe} from "src/IScribe.sol"; import {IScribeTest} from "./IScribeTest.sol"; +import {IScribeTestLST} from "./IScribeTestLST.sol"; +import {IScribeOptimisticTestLST} from "./IScribeOptimisticTestLST.sol"; import {IScribeInvariantTest} from "./invariants/IScribeInvariantTest.sol"; import {ScribeHandler} from "./invariants/ScribeHandler.sol"; @@ -26,6 +30,12 @@ contract ScribeInvariantTest is IScribeInvariantTest { } } +contract ScribeTestLST is IScribeTestLST { + function setUp() public { + setUp(address(new ScribeLST(address(this), "ETH/USD"))); + } +} + // -- Test: Optimistic Scribe -- import {ScribeOptimistic} from "src/ScribeOptimistic.sol"; @@ -39,6 +49,14 @@ contract ScribeOptimisticTest is IScribeOptimisticTest { } } +contract ScribeOptimisticTestLST is IScribeOptimisticTestLST { + function setUp() public { + setUp( + payable(address(new ScribeOptimisticLST(address(this), "ETH/USD"))) + ); + } +} + // -- Test: Libraries -- import {LibSecp256k1Test as LibSecp256k1Test_} from "./LibSecp256k1Test.sol";