-
Notifications
You must be signed in to change notification settings - Fork 16
feat: Implement Djed Tefnut protocol #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add DjedTefnut.sol: simplified Djed Shu without reserve ratios and linear treasury fee - Add deployment scripts for network and mock oracle deployments
WalkthroughThis pull request introduces the DjedTefnut protocol, a new decentralized stablecoin and reserve coin trading system. It includes a core contract with oracle-backed pricing, treasury integration, and trading mechanics, along with two deployment scripts and a Git submodule dependency. Changes
Sequence DiagramsequenceDiagram
actor User
participant DjedTefnut
participant Oracle
participant Treasury
participant StableCoin
User->>DjedTefnut: buyStableCoins(receiver, feeUi, ui) {payable}
activate DjedTefnut
DjedTefnut->>Oracle: Query current payment & price
activate Oracle
Oracle-->>DjedTefnut: Return oracle data
deactivate Oracle
Note over DjedTefnut: Calculate SC amount<br/>Apply fee truncation<br/>Validate TX_LIMIT & threshold
DjedTefnut->>DjedTefnut: deductFees() → net value
DjedTefnut->>Treasury: Transfer treasury fee
DjedTefnut->>User: Transfer UI fee
DjedTefnut->>StableCoin: Mint SC to receiver
activate StableCoin
StableCoin-->>DjedTefnut: Confirm mint
deactivate StableCoin
DjedTefnut->>User: Emit BoughtStableCoins event
deactivate DjedTefnut
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~50 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/DjedTefnut.sol (1)
35-57: Add constructor parameter validation (prevents div-by-zero + misconfigured fees/oracle).
At minimum, consider guarding:oracleAddress != address(0),SCALING_FACTOR != 0,TREASURY != address(0), andFEE + TREASURY_FEE <= SCALING_FACTOR(since UI fee is additive on top at runtime). Also worth confirming whetheroracle.acceptTermsOfService()(Line 56) is valid when called by the protocol contract (vs deployer EOA).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
.gitmodules(1 hunks)scripts/deployDjedTefnutContract.sol(1 hunks)scripts/deployDjedTefnutWithMockOracle.sol(1 hunks)src/DjedTefnut.sol(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: negimox
Repo: DjedAlliance/Djed-Solidity PR: 41
File: src/DjedTefnut.sol:214-217
Timestamp: 2025-12-13T17:46:15.360Z
Learning: In Djed protocol implementations (e.g., DjedTefnut.sol), push payments are used intentionally to maintain consistency with the original Djed protocol architecture, rather than pull payment patterns.
📚 Learning: 2025-12-13T17:46:15.360Z
Learnt from: negimox
Repo: DjedAlliance/Djed-Solidity PR: 41
File: src/DjedTefnut.sol:214-217
Timestamp: 2025-12-13T17:46:15.360Z
Learning: In src/DjedTefnut.sol (Djed protocol implementation), ensure that payments are pushed rather than pulled to align with the original Djed architecture. This pattern should be maintained for consistency with the protocol design; avoid introducing pull payment patterns in this file unless there is a clear, documented justification and impact analysis.
Applied to files:
src/DjedTefnut.sol
🔇 Additional comments (4)
scripts/deployDjedTefnutContract.sol (2)
40-40: Confirmconsole.logavailability in this repo’s Foundry setup.
If this repo standardizes onconsole2, this may fail to compile.
9-38: No changes needed. The tuple unpacking order indeployDjedTefnutContract.solmatches both the return order fromgetConfigFromNetwork()and theDjedTefnutconstructor parameter order exactly. Fee and limit units are consistent (all use appropriate scientific notation scaling). There is no silent misconfiguration risk.src/DjedTefnut.sol (2)
1-8: Standardize SPDX + pragma + OZ import path to match repo tooling.
// SPDX-License-Identifier: AEL(Line 1) is atypical; many toolchains expect SPDX identifiers from the SPDX list.pragma solidity ^0.8.0(Line 2) is looser than the scripts (0.8.19) and can introduce inconsistent compilation unless the repo pins via Foundry config.- Please verify
@openzeppelin/contracts/security/ReentrancyGuard.sol(Line 5) matches the vendored OZ version/remappings used by this repo.
172-175: Push-payment ETH transfers are consistent with the repo’s Djed architecture.
Keeping payouts as “push” transfers here matches the established pattern (and the addednonReentrantprotection is the right complement). Based on learnings, I would not switch this to pull-payments without a clear protocol-level rationale.
| [submodule "lib/hebeswap-contract"] | ||
| path = lib/hebeswap-contract | ||
| url = https://github.com/HebePlatform/Oracle.git | ||
| url = https://github.com/HebePlatform/Oracle.git | ||
| [submodule "lib/chainlink-brownie-contracts"] | ||
| path = lib/chainlink-brownie-contracts | ||
| url = https://github.com/smartcontractkit/chainlink-brownie-contracts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
git submodule statusRepository: DjedAlliance/Djed-Solidity
Length of output: 368
🏁 Script executed:
cat .gitmodulesRepository: DjedAlliance/Djed-Solidity
Length of output: 764
🏁 Script executed:
# Check if there are any references to chainlink or brownie in the codebase
rg -i "chainlink|brownie" --type-list | head -20Repository: DjedAlliance/Djed-Solidity
Length of output: 459
🏁 Script executed:
# Look for Solidity imports that might reference the chainlink contracts
rg "import.*chainlink" --type solidity -iRepository: DjedAlliance/Djed-Solidity
Length of output: 102
🏁 Script executed:
# Check if lib/chainlink-brownie-contracts is referenced in any config files
fd -t f "(foundry|remappings|brownie)" | xargs cat 2>/dev/nullRepository: DjedAlliance/Djed-Solidity
Length of output: 566
🏁 Script executed:
# Check if the submodule directory exists
ls -la lib/chainlink-brownie-contracts 2>&1 || echo "Directory does not exist"Repository: DjedAlliance/Djed-Solidity
Length of output: 173
🏁 Script executed:
# Search for any references to chainlink-brownie-contracts in the codebase
rg "chainlink-brownie" -iRepository: DjedAlliance/Djed-Solidity
Length of output: 52
🏁 Script executed:
# Check if there's a .gitmodules entry that's not initialized
git config --file .gitmodules --get-all submodule.lib/chainlink-brownie-contracts.pathRepository: DjedAlliance/Djed-Solidity
Length of output: 101
🏁 Script executed:
# Look for brownie or chainlink references in source files
fd -e sol -e sol -e js -e ts -e json -e toml -e yaml | xargs rg "chainlink" 2>/dev/null | head -20Repository: DjedAlliance/Djed-Solidity
Length of output: 1598
Remove unused lib/chainlink-brownie-contracts submodule from .gitmodules.
This submodule entry is not initialized, not referenced anywhere in the codebase, and redundant—Chainlink contracts are already pulled from npm (@0.6.1) with remapping configured in foundry.toml. Either remove this entry entirely or clarify if it serves a specific purpose not evident in the current setup.
🤖 Prompt for AI Agents
In .gitmodules around lines 13 to 18, there is an unused submodule entry for
lib/chainlink-brownie-contracts that is not initialized or referenced and
duplicates Chainlink artifacts obtained via npm; remove the entire
lib/chainlink-brownie-contracts submodule entry from .gitmodules and, if
present, delete any corresponding section in .git/config and the
lib/chainlink-brownie-contracts folder (or run git submodule deinit --force --
lib/chainlink-brownie-contracts; git rm --cached
lib/chainlink-brownie-contracts; rm -rf
.git/modules/lib/chainlink-brownie-contracts) so the repo no longer references
this submodule, keeping the existing Chainlink npm dependency and foundry
remappings intact.
| function run() external { | ||
| uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); | ||
| vm.startBroadcast(senderPrivateKey); | ||
|
|
||
| // Deploy MockShuOracle with initial price of $1 (1e18) | ||
| MockShuOracle oracle = new MockShuOracle(1e18); | ||
| console.log("MockShuOracle deployed at:", address(oracle)); | ||
|
|
||
| // Deploy DjedTefnut with mock oracle | ||
| DjedTefnut djedTefnut = new DjedTefnut( | ||
| address(oracle), // oracle address | ||
| 1e18, // scalingFactor (1.0) | ||
| msg.sender, // treasury address (deployer) | ||
| 100, // treasuryFee (1%) | ||
| 200, // fee (2%) | ||
| 1000e18, // thresholdSupplySc (1000 stable coins) | ||
| 1e15, // rcMinPrice (0.001) | ||
| 1e17, // rcInitialPrice (0.1) | ||
| 100e18 // txLimit (100 ETH) | ||
| ); | ||
|
|
||
| console.log("DjedTefnut deployed at:", address(djedTefnut)); | ||
| vm.stopBroadcast(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix treasury address + align fee units with scalingFactor (deployment would misconfigure protocol).
msg.sender(Line 21) in a Forge script is commonly the script contract address, not the broadcaster EOA; you likely want the broadcast signer address.- With
scalingFactor = 1e18(Line 20),treasuryFee = 100andfee = 200(Line 22-23) don’t represent 1%/2% under a “scaled decimal” convention; they’ll be effectively ~0.
Proposed fix:
function run() external {
uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY");
+ address deployer = vm.addr(senderPrivateKey);
vm.startBroadcast(senderPrivateKey);
// Deploy DjedTefnut with mock oracle
DjedTefnut djedTefnut = new DjedTefnut(
address(oracle), // oracle address
1e18, // scalingFactor (1.0)
- msg.sender, // treasury address (deployer)
- 100, // treasuryFee (1%)
- 200, // fee (2%)
+ deployer, // treasury address (deployer)
+ 1e16, // treasuryFee (1%) if scalingFactor=1e18
+ 2e16, // fee (2%) if scalingFactor=1e18
1000e18, // thresholdSupplySc (1000 stable coins)
1e15, // rcMinPrice (0.001)
1e17, // rcInitialPrice (0.1)
100e18 // txLimit (100 ETH)
);
}Also: confirm console.log compiles in your setup (some setups require console2.log or an explicit console import).
🤖 Prompt for AI Agents
scripts/deployDjedTefnutWithMockOracle.sol around lines 9 to 31: the script
passes msg.sender as the treasury address and uses unscaled integer fees (100,
200) while scalingFactor is 1e18, which will misconfigure the protocol; change
the treasury address to the broadcaster EOA by deriving it from the private key
(e.g., vm.addr(senderPrivateKey)) instead of msg.sender, and convert percent
fees to scaled decimals matching scalingFactor (1% => 1e16, 2% => 2e16) so
treasuryFee and fee are set to 1e16 and 2e16 respectively; also verify your
environment supports console.log (import or switch to console2.log if required).
| function R(uint256 _currentPaymentAmount) public view returns (uint256) { | ||
| return address(this).balance - _currentPaymentAmount; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: buy* misprices / can underflow because fees are transferred out before calling sc*Price(msg.value).
After deductFees() (Line 83/104) sends ETH out, address(this).balance may become < msg.value, so R(msg.value) (Line 61) can revert. Even when it doesn’t revert, it’s no longer computing “reserve before current payment” consistently.
Suggested fix: compute prices first, then transfer fees, then mint using the post-fee amountBc.
function buyStableCoins(address receiver, uint256 feeUi, address ui) external payable nonReentrant {
oracle.updateOracleValues();
- uint256 amountBc = deductFees(msg.value, feeUi, ui);
- uint256 amountSc = (amountBc * SC_DECIMAL_SCALING_FACTOR) / scMaxPrice(msg.value);
+ uint256 price = scMaxPrice(msg.value);
+ uint256 amountBc = deductFees(msg.value, feeUi, ui);
+ uint256 amountSc = (amountBc * SC_DECIMAL_SCALING_FACTOR) / price;
require(amountSc <= TX_LIMIT || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "buySC: tx limit exceeded");
require(amountSc > 0, "buySC: receiving zero SCs");
stableCoin.mint(receiver, amountSc);
- emit BoughtStableCoins(msg.sender, receiver, amountSc, msg.value);
+ emit BoughtStableCoins(msg.sender, receiver, amountSc, amountBc);
}
function buyReserveCoins(address receiver, uint256 feeUi, address ui) external payable nonReentrant {
oracle.updateOracleValues();
- uint256 amountBc = deductFees(msg.value, feeUi, ui);
- uint256 minPrice = scMinPrice(msg.value);
+ uint256 minPrice = scMinPrice(msg.value);
+ uint256 rcPrice = rcBuyingPrice(minPrice, msg.value);
+ uint256 amountBc = deductFees(msg.value, feeUi, ui);
require(amountBc <= (TX_LIMIT * minPrice) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "buyRC: tx limit exceeded");
- uint256 amountRc = (amountBc * RC_DECIMAL_SCALING_FACTOR) / rcBuyingPrice(minPrice, msg.value);
+ uint256 amountRc = (amountBc * RC_DECIMAL_SCALING_FACTOR) / rcPrice;
require(amountRc > 0, "buyRC: receiving zero RCs");
reserveCoin.mint(receiver, amountRc);
- emit BoughtReserveCoins(msg.sender, receiver, amountRc, msg.value);
+ emit BoughtReserveCoins(msg.sender, receiver, amountRc, amountBc);
}
function deductFees(uint256 value, uint256 feeUi, address ui) internal returns (uint256) {
+ if (feeUi > 0) require(ui != address(0), "ui required when feeUi > 0");
uint256 totalFees = ((value * FEE) + (value * feeUi) + (value * TREASURY_FEE)) / SCALING_FACTOR;
transfer(TREASURY, (value * TREASURY_FEE) / SCALING_FACTOR);
transfer(ui, (value * feeUi) / SCALING_FACTOR);
return value - totalFees;
}Also applies to: 81-89, 102-111, 127-132
| function sellStableCoins(uint256 amountSc, address receiver, uint256 feeUi, address ui) external nonReentrant { | ||
| oracle.updateOracleValues(); | ||
| require(stableCoin.balanceOf(msg.sender) >= amountSc, "sellSC: insufficient SC balance"); | ||
| require(amountSc <= TX_LIMIT || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellSC: tx limit exceeded"); | ||
| stableCoin.burn(msg.sender, amountSc); | ||
| uint256 amountBc = deductFees((amountSc * scMinPrice(0)) / SC_DECIMAL_SCALING_FACTOR, feeUi, ui); | ||
| require(amountBc > 0, "sellSC: receiving zero BCs"); | ||
| transfer(receiver, amountBc); | ||
| emit SoldStableCoins(msg.sender, receiver, amountSc, amountBc); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: sell* overpays by burning before pricing (and RC recomputes price after burn).
sellStableCoins: burn happens beforescMinPrice(0)(Line 95-96), increasingscPriceand overpaying sellers.sellReserveCoins:rcTargetPrice(...)is called again after burn (Line 118-120), which can significantly increase payout.
Suggested fix: compute price (and gross payout) first, then burn, then deduct fees and transfer.
function sellStableCoins(uint256 amountSc, address receiver, uint256 feeUi, address ui) external nonReentrant {
oracle.updateOracleValues();
require(stableCoin.balanceOf(msg.sender) >= amountSc, "sellSC: insufficient SC balance");
require(amountSc <= TX_LIMIT || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellSC: tx limit exceeded");
- stableCoin.burn(msg.sender, amountSc);
- uint256 amountBc = deductFees((amountSc * scMinPrice(0)) / SC_DECIMAL_SCALING_FACTOR, feeUi, ui);
+ uint256 price = scMinPrice(0);
+ uint256 grossBc = (amountSc * price) / SC_DECIMAL_SCALING_FACTOR;
+ stableCoin.burn(msg.sender, amountSc);
+ uint256 amountBc = deductFees(grossBc, feeUi, ui);
require(amountBc > 0, "sellSC: receiving zero BCs");
transfer(receiver, amountBc);
emit SoldStableCoins(msg.sender, receiver, amountSc, amountBc);
}
function sellReserveCoins(uint256 amountRc, address receiver, uint256 feeUi, address ui) external nonReentrant {
oracle.updateOracleValues();
require(reserveCoin.balanceOf(msg.sender) >= amountRc, "sellRC: insufficient RC balance");
uint256 maxPrice = scMaxPrice(0);
- require((amountRc * rcTargetPrice(maxPrice, 0)) / RC_DECIMAL_SCALING_FACTOR <= (TX_LIMIT * maxPrice) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellRC: tx limit exceeded");
- reserveCoin.burn(msg.sender, amountRc);
- uint256 amountBc = deductFees((amountRc * rcTargetPrice(maxPrice, 0)) / RC_DECIMAL_SCALING_FACTOR, feeUi, ui);
+ uint256 rcPrice = rcTargetPrice(maxPrice, 0);
+ uint256 grossBc = (amountRc * rcPrice) / RC_DECIMAL_SCALING_FACTOR;
+ require(grossBc <= (TX_LIMIT * maxPrice) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellRC: tx limit exceeded");
+ reserveCoin.burn(msg.sender, amountRc);
+ uint256 amountBc = deductFees(grossBc, feeUi, ui);
require(amountBc > 0, "sellRC: receiving zero BCs");
transfer(receiver, amountBc);
emit SoldReserveCoins(msg.sender, receiver, amountRc, amountBc);
}Also applies to: 113-123, 156-170
implement: #37
Summary by CodeRabbit
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.