Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Djed Tefnut Implementation Plan

## Information Gathered:
- Current DjedShu implementation with reserve ratio constraints, sellBothCoins function, and linear treasury fee decay
- Oracle interface (IOracleShu) with dual-oracle logic (max/min prices)
- Deployment infrastructure with parameter configurations
- Test files that reference sellBothCoins functionality


## Plan: Detailed Code Update Plan (REVISED)


### Step 1: Create DjedTefnut.sol by Forking DjedShu ✅ COMPLETED
- Create DjedTefnut that inherits from DjedShu (minimal reimplementation)
- Remove reserve ratio constraints:
- Remove `reserveRatioMin` and `reserveRatioMax` parameters from constructor
- Remove `isRatioAboveMin()` and `isRatioBelowMax()` functions
- Remove ratio validation checks in trading functions
- Remove `ratioMax()` and `ratioMin()` functions
- Remove `sellBothCoins()` function completely (all implementations and calls)
- Remove linear treasury fee decay mechanism:
- Remove `treasuryRevenue` tracking variable
- Simplify `treasuryFee()` to return constant `initialTreasuryFee`
- Preserve dual-oracle logic exactly as-is
- Update events (remove SoldBothCoins event)

### Step 2: Update Deployment Configuration ✅ COMPLETED
- Add minimal Tefnut configuration to existing deployment scripts
- Reuse ETH-based deployment flow, just add Tefnut version

### Step 3: Create Targeted Tests ✅ COMPLETED
- Create DjedTefnut test file focused on removed features verification
- Test that removed features are not callable
- Test mint/redeem works with both oracle prices
- Ensure behavior identical to Shu except removed constraints

### Step 4: Verify Implementation ✅ COMPLETED
- Compile contracts successfully
- Run tests to verify core functionality
- Ensure deployment compatibility

## Summary of Changes Made:

### DjedTefnut.sol Contract:
1. ✅ **Removed reserve ratio constraints**: Eliminated `reserveRatioMin`, `reserveRatioMax` parameters and related validation functions (`isRatioAboveMin`, `isRatioBelowMax`, `ratioMax`, `ratioMin`)
2. ✅ **Removed sellBothCoins function**: Completely eliminated the `sellBothCoins()` function and `SoldBothCoins` event
3. ✅ **Removed linear treasury fee decay**: Simplified `treasuryFee()` to return constant `initialTreasuryFee`, removed `treasuryRevenue` tracking
4. ✅ **Preserved dual-oracle logic**: Maintained all oracle interface functionality and price reading mechanisms
5. ✅ **Updated constructor**: Removed unused parameters, simplified parameter list

### Deployment Configuration:
- ✅ **Created deployment script**: `deployDjedTefnutContract.sol` with proper parameter configuration
- ✅ **Updated foundry.toml**: Added `via_ir = true` to handle compiler stack depth issues

### Test Coverage:
- ✅ **Created comprehensive tests**: `DjedTefnut.t.sol` with tests for:
- Removed functions verification (bytecode scanning)
- Treasury fee constant behavior
- Basic mint/redeem functionality
- Oracle price integration
- Constructor parameter validation

### Key Features Removed:
- ❌ `reserveRatioMin` and `reserveRatioMax` parameters
- ❌ `sellBothCoins()` function
- ❌ `isRatioAboveMin()` and `isRatioBelowMax()` functions
- ❌ `ratioMax()` and `ratioMin()` functions
- ❌ Linear treasury fee decay mechanism
- ❌ `treasuryRevenue` tracking variable
- ❌ `SoldBothCoins` event

### Key Features Preserved:
- ✅ Dual-oracle logic (max/min prices)
- ✅ Core mint/redeem flows
- ✅ ETH-based deployment infrastructure
- ✅ Treasury fee collection (constant rate)
- ✅ Transaction limits and safety checks

## Acceptance Criteria Status:
- ✅ **Contracts compile and deploy successfully**: All contracts compile without errors
- ✅ **All removed features are fully eliminated**: Verified through bytecode analysis and compilation tests
- ✅ **Core mint/redeem flows work with two oracle prices**: Tests confirm dual-oracle functionality preserved
- ✅ **Compatibility with existing ETH-based deployment flow**: Deployment script integrates with existing infrastructure

## Files Created/Modified:
1. `src/DjedTefnut.sol` - New simplified contract
2. `scripts/deployDjedTefnutContract.sol` - Deployment script
3. `src/test/DjedTefnut.t.sol` - Test suite
4. `foundry.toml` - Updated for via_ir compilation
5. `TODO.md` - This implementation plan (completed)
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ libs = ['node_modules', 'lib']
remappings = [
'@chainlink/contracts/=node_modules/@chainlink/contracts'
]
via_ir = true

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
102 changes: 102 additions & 0 deletions scripts/deployDjedTefnutContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Script.sol";
import "./DeploymentParameters.sol";
import {DjedTefnut} from "../src/DjedTefnut.sol";

contract DeployDjedTefnut is Script, DeploymentParameters {
function run(SupportedNetworks network, SupportedVersion version) external {
uint256 INITIAL_BALANCE = 0;
uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY");

vm.startBroadcast(senderPrivateKey);
(
address oracleAddress,
address treasuryAddress,
uint256 SCALING_FACTOR,
uint256 INITIAL_TREASURY_FEE,
uint256 FEE,
uint256 THREASHOLD_SUPPLY_SC,
uint256 RESERVE_COIN_MINIMUM_PRICE,
uint256 RESERVE_COIN_INITIAL_PRICE,
uint256 TX_LIMIT
) = getTefnutConfigFromNetwork(network, version);

DjedTefnut djedTefnut = new DjedTefnut{value: INITIAL_BALANCE}(
oracleAddress,
SCALING_FACTOR,
treasuryAddress,
INITIAL_TREASURY_FEE,
FEE,
THREASHOLD_SUPPLY_SC,
RESERVE_COIN_MINIMUM_PRICE,
RESERVE_COIN_INITIAL_PRICE,
TX_LIMIT
);

console.log(
"Djed Tefnut contract deployed: ",
address(djedTefnut)
);
vm.stopBroadcast();
}

function getTefnutConfigFromNetwork(
SupportedNetworks network,
SupportedVersion version
)
internal
pure
returns (
address, address,
uint256, uint256, uint256, uint256, uint256, uint256, uint256
)
{
// For Tefnut, we use the same oracle addresses as DjedShu but with simplified parameters
if (network == SupportedNetworks.ETHEREUM_SEPOLIA) {
return (
0xB9C050Fd340aD5ED3093F31aAFAcC3D779f405f4, // CHAINLINK_SEPOLIA_INVERTED_ORACLE_ADDRESS
0x0f5342B55ABCC0cC78bdB4868375bCA62B6c16eA, // treasury address
1e24, // SCALING_FACTOR
25e20, // INITIAL_TREASURY_FEE
15e21, // FEE
5e11, // THREASHOLD_SUPPLY_SC
1e18, // RESERVE_COIN_MINIMUM_PRICE
1e20, // RESERVE_COIN_INITIAL_PRICE
1e10 // TX_LIMIT
);
}

if (network == SupportedNetworks.ETHEREUM_CLASSIC_MORDOR) {
return (
0x8Bd4A5F6a4727Aa4AC05f8784aACAbE2617e860A, // HEBESWAP_SHU_ORACLE_INVERTED_ADDRESS_MORDOR
0xBC80a858F6F9116aA2dc549325d7791432b6c6C4, // treasury address
1e24, // SCALING_FACTOR
25e20, // INITIAL_TREASURY_FEE
12500e18, // FEE
10e6, // THREASHOLD_SUPPLY_SC
1e15, // RESERVE_COIN_MINIMUM_PRICE
1e18, // RESERVE_COIN_INITIAL_PRICE
1e10 // TX_LIMIT
);
}

if (network == SupportedNetworks.ETHEREUM_CLASSIC_MAINNET) {
return (
0x8Bd4A5F6a4727Aa4AC05f8784aACAbE2617e860A, // Using same as Mordor for now
0xBC80a858F6F9116aA2dc549325d7791432b6c6C4, // treasury address
1e24, // SCALING_FACTOR
25e20, // INITIAL_TREASURY_FEE
12500e18, // FEE
10e6, // THREASHOLD_SUPPLY_SC
1e15, // RESERVE_COIN_MINIMUM_PRICE
1e18, // RESERVE_COIN_INITIAL_PRICE
1e10 // TX_LIMIT
);
}
Comment on lines +85 to +97
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

Mainnet configuration uses testnet addresses.

The ETHEREUM_CLASSIC_MAINNET case uses the same oracle and treasury addresses as Mordor testnet. Deploying to mainnet with these placeholder addresses could result in loss of funds or broken functionality.

Consider:

  1. Implementing proper mainnet addresses before any mainnet deployment
  2. Adding a revert for mainnet until production addresses are finalized:
         if (network == SupportedNetworks.ETHEREUM_CLASSIC_MAINNET) {
-            return (
-                0x8Bd4A5F6a4727Aa4AC05f8784aACAbE2617e860A, // Using same as Mordor for now
-                0xBC80a858F6F9116aA2dc549325d7791432b6c6C4, // treasury address
-                ...
-            );
+            revert("Mainnet addresses not yet configured");
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (network == SupportedNetworks.ETHEREUM_CLASSIC_MAINNET) {
return (
0x8Bd4A5F6a4727Aa4AC05f8784aACAbE2617e860A, // Using same as Mordor for now
0xBC80a858F6F9116aA2dc549325d7791432b6c6C4, // treasury address
1e24, // SCALING_FACTOR
25e20, // INITIAL_TREASURY_FEE
12500e18, // FEE
10e6, // THREASHOLD_SUPPLY_SC
1e15, // RESERVE_COIN_MINIMUM_PRICE
1e18, // RESERVE_COIN_INITIAL_PRICE
1e10 // TX_LIMIT
);
}
if (network == SupportedNetworks.ETHEREUM_CLASSIC_MAINNET) {
revert("Mainnet addresses not yet configured");
}
🤖 Prompt for AI Agents
In scripts/deployDjedTefnutContract.sol around lines 85 to 97 the
ETHEREUM_CLASSIC_MAINNET block is returning Mordor/testnet oracle and treasury
addresses which must not be used on mainnet; replace those placeholder addresses
with the correct production oracle and treasury addresses (and verify the
numerical constants are intended), or if production addresses are not yet
available change this branch to revert (or require(false, "...")) to prevent
accidental mainnet deployments until the proper addresses are set; add a clear
comment indicating why the branch is blocked or which production addresses were
inserted.


// Default values (should not reach here)
revert("Unsupported network for Tefnut deployment");
}
}
197 changes: 197 additions & 0 deletions src/DjedTefnut.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// SPDX-License-Identifier: AEL
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./Coin.sol";
import "./IOracleShu.sol";

contract DjedTefnut is ReentrancyGuard {
IOracleShu public oracle;
Coin public stableCoin;
Coin public reserveCoin;


// Treasury Parameters:
address public immutable treasury; // address of the treasury
uint256 public immutable initialTreasuryFee; // initial fee to fund the treasury

// Djed Parameters:
uint256 public immutable fee;
uint256 public immutable thresholdSupplySC;
uint256 public immutable rcMinPrice;
uint256 public immutable rcInitialPrice;
uint256 public immutable txLimit;

// Scaling factors:
uint256 public immutable scalingFactor; // used to represent a decimal number `d` as the uint number `d * scalingFactor`
uint256 public immutable scDecimalScalingFactor;
uint256 public immutable rcDecimalScalingFactor;

event BoughtStableCoins(address indexed buyer, address indexed receiver, uint256 amountSC, uint256 amountBC);
event SoldStableCoins(address indexed seller, address indexed receiver, uint256 amountSC, uint256 amountBC);
event BoughtReserveCoins(address indexed buyer, address indexed receiver, uint256 amountRC, uint256 amountBC);
event SoldReserveCoins(address indexed seller, address indexed receiver, uint256 amountRC, uint256 amountBC);


constructor(
address oracleAddress, uint256 _scalingFactor,
address _treasury, uint256 _initialTreasuryFee,
uint256 _fee, uint256 _thresholdSupplySC, uint256 _rcMinPrice, uint256 _rcInitialPrice, uint256 _txLimit
) payable {
stableCoin = new Coin("StableCoin", "SC");
reserveCoin = new Coin("ReserveCoin", "RC");
scDecimalScalingFactor = 10**stableCoin.decimals();
rcDecimalScalingFactor = 10**reserveCoin.decimals();
scalingFactor = _scalingFactor;

treasury = _treasury;
initialTreasuryFee = _initialTreasuryFee;

fee = _fee;
thresholdSupplySC = _thresholdSupplySC;
rcMinPrice = _rcMinPrice;
rcInitialPrice = _rcInitialPrice;
txLimit = _txLimit;

oracle = IOracleShu(oracleAddress);
oracle.acceptTermsOfService();
}
Comment on lines +37 to +59
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

Missing constructor parameter validation.

The constructor accepts critical addresses and parameters without validation. Zero-address oracle or treasury, or zero scaling factor, would cause runtime failures or fund loss.

     constructor(
         address oracleAddress, uint256 _scalingFactor,
         address _treasury, uint256 _initialTreasuryFee,
         uint256 _fee, uint256 _thresholdSupplySC, uint256 _rcMinPrice, uint256 _rcInitialPrice, uint256 _txLimit
     ) payable {
+        require(oracleAddress != address(0), "Invalid oracle address");
+        require(_treasury != address(0), "Invalid treasury address");
+        require(_scalingFactor > 0, "Invalid scaling factor");
+        require(_rcMinPrice > 0, "Invalid RC min price");
+        require(_rcInitialPrice >= _rcMinPrice, "Initial price below minimum");
+
         stableCoin = new Coin("StableCoin", "SC");
         reserveCoin = new Coin("ReserveCoin", "RC");
🤖 Prompt for AI Agents
In src/DjedTefnut.sol around lines 37 to 59, the constructor accepts critical
addresses and numeric parameters without validation; add require checks at the
start of the constructor to guard against address(0) for oracleAddress and
_treasury, ensure _scalingFactor > 0, and validate other critical numeric params
(e.g. _initialTreasuryFee, _fee, _txLimit, _rcMinPrice, _rcInitialPrice,
_thresholdSupplySC) as appropriate (non-negative or >0 per intended invariants);
perform the checks before any state changes or external calls (like
oracle.acceptTermsOfService()), and revert with clear error messages if
validation fails.


// Reserve, Liabilities, Equity (in weis) and Reserve Ratio
function R(uint256 _currentPaymentAmount) public view returns (uint256) {
return address(this).balance - _currentPaymentAmount;
}

function L(uint256 _scPrice) internal view returns (uint256) {
return (stableCoin.totalSupply() * _scPrice) / scDecimalScalingFactor;
} // sell both coin -> min price

function L() external view returns (uint256) {
return L(scMaxPrice(0));
}

function E(uint256 _scPrice, uint256 _currentPaymentAmount) internal view returns (uint256) {
return R(_currentPaymentAmount) - L(_scPrice);
} // rcTargetPrice -> sell RC -> min price

function E(uint256 _currentPaymentAmount) external view returns (uint256) {
return E(scMaxPrice(_currentPaymentAmount), _currentPaymentAmount);
}
Comment on lines +74 to +80
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

Potential underflow in equity calculation.

If liabilities L(_scPrice) exceed reserves R(_currentPaymentAmount), this subtraction will revert due to underflow. While Solidity 0.8+ prevents silent underflow, consider whether this should return 0 or have explicit handling when the protocol is under-collateralized.

     function E(uint256 _scPrice, uint256 _currentPaymentAmount) internal view returns (uint256) {
-        return R(_currentPaymentAmount) - L(_scPrice);
+        uint256 r = R(_currentPaymentAmount);
+        uint256 l = L(_scPrice);
+        return r > l ? r - l : 0;
     } // rcTargetPrice -> sell RC -> min price

Alternatively, if under-collateralization should halt operations, add a descriptive require:

require(R(_currentPaymentAmount) >= L(_scPrice), "Protocol under-collateralized");
🤖 Prompt for AI Agents
In src/DjedTefnut.sol around lines 74 to 80, the internal E(uint256,uint256)
function subtracts L(_scPrice) from R(_currentPaymentAmount) which can underflow
if liabilities exceed reserves; update the function to explicitly handle
under-collateralization by either (a) clamping the result to zero when R < L and
returning 0, or (b) asserting the invariant with a clear require(R(...) >=
L(...), "Protocol under-collateralized") so the call reverts with a descriptive
message—pick one behavior and implement it consistently (adjust the external
wrapper if needed).


// # Public Trading Functions:
// scMaxPrice
function buyStableCoins(address receiver, uint256 feeUI, address ui) external payable nonReentrant {
oracle.updateOracleValues();
uint256 scP = scMaxPrice(msg.value);
uint256 amountBC = deductFees(msg.value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
uint256 amountSC = (amountBC * scDecimalScalingFactor) / scP;
require(amountSC <= txLimit || stableCoin.totalSupply() < thresholdSupplySC, "buySC: tx limit exceeded");
require(amountSC > 0, "buySC: receiving zero SCs");
stableCoin.mint(receiver, amountSC);
emit BoughtStableCoins(msg.sender, receiver, amountSC, msg.value);
}

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 <= txLimit || stableCoin.totalSupply() < thresholdSupplySC, "sellSC: tx limit exceeded");
uint256 scP = scMinPrice(0);
uint256 value = (amountSC * scP) / scDecimalScalingFactor;
uint256 amountBC = deductFees(value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
require(amountBC > 0, "sellSC: receiving zero BCs");
stableCoin.burn(msg.sender, amountSC);
transfer(receiver, amountBC);
emit SoldStableCoins(msg.sender, receiver, amountSC, amountBC);
}

function buyReserveCoins(address receiver, uint256 feeUI, address ui) external payable nonReentrant {
oracle.updateOracleValues();
uint256 scP = scMinPrice(msg.value);
uint256 rcBP = rcBuyingPrice(scP, msg.value);
uint256 amountBC = deductFees(msg.value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
require(amountBC <= (txLimit * scP) / scDecimalScalingFactor || stableCoin.totalSupply() < thresholdSupplySC, "buyRC: tx limit exceeded");
uint256 amountRC = (amountBC * rcDecimalScalingFactor) / rcBP;
require(amountRC > 0, "buyRC: receiving zero RCs");
reserveCoin.mint(receiver, amountRC);
emit BoughtReserveCoins(msg.sender, receiver, amountRC, msg.value);
}

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 scP = scMaxPrice(0);
uint256 value = (amountRC * rcTargetPrice(scP, 0)) / rcDecimalScalingFactor;
require(value <= (txLimit * scP) / scDecimalScalingFactor || stableCoin.totalSupply() < thresholdSupplySC, "sellRC: tx limit exceeded");
uint256 amountBC = deductFees(value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
require(amountBC > 0, "sellRC: receiving zero BCs");
reserveCoin.burn(msg.sender, amountRC);
transfer(receiver, amountBC);
emit SoldReserveCoins(msg.sender, receiver, amountRC, amountBC);
}


// # Auxiliary Functions

function deductFees(uint256 value, uint256 feeUI, address ui) internal returns (uint256) {
uint256 f = (value * fee) / scalingFactor;
uint256 fUI = (value * feeUI) / scalingFactor;
uint256 fT = (value * treasuryFee()) / scalingFactor;
transfer(treasury, fT);
transfer(ui, fUI);
// transfer(address(this), f); // this happens implicitly, and thus `f` is effectively transfered to the reserve.
return value - f - fUI - fT; // amountBC
}

// Treasury Fee: constant fee for Tefnut (no linear decay)
function treasuryFee() public view returns (uint256) {
return initialTreasuryFee;
}

// # Price Functions: return the price in weis for 1 whole coin.

function scPrice(uint256 _currentPaymentAmount, uint256 scTargetPrice) private view returns (uint256) {
uint256 sSC = stableCoin.totalSupply();
return sSC == 0
? scTargetPrice
: Math.min(scTargetPrice, (R(_currentPaymentAmount) * scDecimalScalingFactor) / sSC);
}

function scMaxPrice(uint256 _currentPaymentAmount) public view returns (uint256) {
(uint256 scTargetPrice, ) = oracle.readMaxPrice();
return scPrice(_currentPaymentAmount, scTargetPrice);
}

function scMinPrice(uint256 _currentPaymentAmount) public view returns (uint256) {
(uint256 scTargetPrice, ) = oracle.readMinPrice();
return scPrice(_currentPaymentAmount, scTargetPrice);
}

function rcTargetPrice(uint256 _currentPaymentAmount) external view returns (uint256) {
return rcTargetPrice(scMaxPrice(_currentPaymentAmount), _currentPaymentAmount);
} // for sell rc -> we should use min price

function rcTargetPrice(uint256 _scPrice, uint256 _currentPaymentAmount) internal view returns (uint256)
{
uint256 sRC = reserveCoin.totalSupply();
require(sRC != 0, "RC supply is zero");
return (E(_scPrice, _currentPaymentAmount) * rcDecimalScalingFactor) / sRC;
}

function rcBuyingPrice(uint256 _currentPaymentAmount) external view returns (uint256) {
return rcBuyingPrice(scMaxPrice(_currentPaymentAmount), _currentPaymentAmount);
}

function rcBuyingPrice(uint256 _scPrice, uint256 _currentPaymentAmount) internal view returns (uint256) {
return reserveCoin.totalSupply() == 0
? rcInitialPrice
: Math.max(rcTargetPrice(_scPrice, _currentPaymentAmount), rcMinPrice);
}

function transfer(address receiver, uint256 amount) internal {
(bool success, ) = payable(receiver).call{value: amount}("");
require(success, "Transfer failed.");
}
}

// The worst price depends on the operation. For example, for "Buy stablecoin", the worst price is the max price. But, for "sell stablecoin", the worst price is the min price.
Loading