Skip to content

Conversation

@kpj2006
Copy link

@kpj2006 kpj2006 commented Dec 13, 2025

implement: #37

  • Add DjedTefnut.sol: simplified Djed Shu without reserve ratios and linear treasury fee
  • Add deployment scripts for network and mock oracle deployments
image

Summary by CodeRabbit

  • New Features

    • Introduced a decentralized stablecoin and reserve coin trading platform with oracle-driven dynamic pricing, transaction limits, and integrated treasury fee mechanisms.
    • Added automated deployment scripts for contract initialization, including a variant with mock oracle support for testing.
  • Chores

    • Added external dependency for Chainlink smart contract libraries.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add DjedTefnut.sol: simplified Djed Shu without reserve ratios and linear treasury fee
- Add deployment scripts for network and mock oracle deployments
@coderabbitai
Copy link

coderabbitai bot commented Dec 13, 2025

Walkthrough

This 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

Cohort / File(s) Summary
Git Dependencies
.gitmodules
Adds new Git submodule lib/chainlink-brownie-contracts pointing to smartcontractkit/chainlink-brownie-contracts; minor formatting adjustment to hebeswap-contract submodule entry.
Deployment Automation
scripts/deployDjedTefnutContract.sol, scripts/deployDjedTefnutWithMockOracle.sol
Two new Solidity Forge scripts: DeployDjedTefnut retrieves deployment config from network and deploys the main contract; DeployDjedTefnutWithMock deploys both a MockShuOracle and the main contract with preset parameters in a single broadcast.
Core Protocol Contract
src/DjedTefnut.sol
New contract implementing oracle-driven stablecoin (SC) and reserve coin (RC) trading with immutable protocol parameters, fee/treasury mechanics, non-reentrant guards, price calculation helpers (R, L, E functions), and four primary trading functions (buy/sell SC and RC).

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • src/DjedTefnut.sol: Dense mathematical logic with multiple price calculation functions (R, L, E, scPrice, rcTargetPrice, rcBuyingPrice). Review requires careful verification of:
    • Arithmetic correctness and overflow/underflow safety in scaling operations
    • Oracle integration and price feed dependencies
    • Fee and treasury deduction mechanics across four similar but distinct trading flows
    • Reentrancy guard placement and transaction limit enforcement
    • Parameter initialization and immutable constant values
  • Deployment scripts: Straightforward but dependency on contract initialization parameters and environment setup should be verified
  • Submodule addition: Minimal complexity but verify compatibility with project

Poem

🐰 A stablecoin hops into the blockchain yard,
With oracle whispers and treasury guard,
Reserve coins dance in the DjedTefnut way,
Trading and pricing throughout the day!
✨ Let the defi dreams hop and sway!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: Implement Djed Tefnut protocol' clearly and directly describes the main change: introducing the Djed Tefnut protocol implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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), and FEE + TREASURY_FEE <= SCALING_FACTOR (since UI fee is additive on top at runtime). Also worth confirming whether oracle.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

📥 Commits

Reviewing files that changed from the base of the PR and between 65fa433 and 715f0cb.

📒 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: Confirm console.log availability in this repo’s Foundry setup.
If this repo standardizes on console2, this may fail to compile.


9-38: No changes needed. The tuple unpacking order in deployDjedTefnutContract.sol matches both the return order from getConfigFromNetwork() and the DjedTefnut constructor 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 added nonReentrant protection is the right complement). Based on learnings, I would not switch this to pull-payments without a clear protocol-level rationale.

Comment on lines 13 to 18
[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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git submodule status

Repository: DjedAlliance/Djed-Solidity

Length of output: 368


🏁 Script executed:

cat .gitmodules

Repository: 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 -20

Repository: DjedAlliance/Djed-Solidity

Length of output: 459


🏁 Script executed:

# Look for Solidity imports that might reference the chainlink contracts
rg "import.*chainlink" --type solidity -i

Repository: 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/null

Repository: 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" -i

Repository: 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.path

Repository: 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 -20

Repository: 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.

Comment on lines +9 to +31
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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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 = 100 and fee = 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).

Comment on lines +60 to +62
function R(uint256 _currentPaymentAmount) public view returns (uint256) {
return address(this).balance - _currentPaymentAmount;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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

Comment on lines +91 to +100
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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: sell* overpays by burning before pricing (and RC recomputes price after burn).

  • sellStableCoins: burn happens before scMinPrice(0) (Line 95-96), increasing scPrice and 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant