Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
85 changes: 85 additions & 0 deletions scripts/DeploymentParametersTefnut.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: AEL
pragma solidity ^0.8.0;

/**
* @title DeploymentParametersTefnut
* @notice Configuration management for DjedTefnut deployment across different chains
* @dev Returns deployment parameters based on chain ID
*/
contract DeploymentParametersTefnut {

/// @notice Struct containing all DjedTefnut constructor parameters
struct Parameters {
address oracleAddress; // Oracle contract address (address(0) signals mock needed)
uint256 scalingFactor; // Scaling factor for decimal representation
address treasury; // Treasury address for fee collection
uint256 treasuryFee; // Fixed treasury fee (no decay)
uint256 fee; // Protocol fee
uint256 thresholdSupplySc; // Threshold supply for stable coins
uint256 rcMinPrice; // Minimum reserve coin price
uint256 rcInitialPrice; // Initial reserve coin price
uint256 txLimit; // Transaction limit
}

// Chain IDs
uint256 constant SEPOLIA_CHAIN_ID = 11155111;
uint256 constant ANVIL_CHAIN_ID = 31337;

// Known oracle addresses
address constant SEPOLIA_SHU_ORACLE_ADDRESS = address(0); // Placeholder - replace with actual address when deployed
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Sepolia will deploy with mock oracle instead of real oracle.

The SEPOLIA_SHU_ORACLE_ADDRESS is set to address(0) as a placeholder. However, the deployment script interprets address(0) as a signal to deploy MockShuOracleTefnut. This means Sepolia deployments will use a mock oracle rather than a production oracle, which may not be intended for testnet deployments.

Update the placeholder comment to clarify this behavior, or replace with the actual Sepolia oracle address when available:

-    address constant SEPOLIA_SHU_ORACLE_ADDRESS = address(0); // Placeholder - replace with actual address when deployed
+    address constant SEPOLIA_SHU_ORACLE_ADDRESS = address(0); // IMPORTANT: address(0) triggers mock oracle deployment. Replace with actual Sepolia oracle address.
📝 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
address constant SEPOLIA_SHU_ORACLE_ADDRESS = address(0); // Placeholder - replace with actual address when deployed
address constant SEPOLIA_SHU_ORACLE_ADDRESS = address(0); // IMPORTANT: address(0) triggers mock oracle deployment. Replace with actual Sepolia oracle address.
🤖 Prompt for AI Agents
In scripts/DeploymentParametersTefnut.sol around line 29, the
SEPOLIA_SHU_ORACLE_ADDRESS is set to address(0) which your deployment script
treats as a signal to deploy MockShuOracleTefnut; update the line comment to
explicitly state that address(0) will cause the mock oracle to be deployed on
Sepolia or, if you intend to use a real oracle, replace address(0) with the
actual Sepolia oracle address when available so the deployment uses the
production oracle instead of the mock.


// Known treasury addresses
address constant SEPOLIA_TREASURY = 0x0f5342B55ABCC0cC78bdB4868375bCA62B6c16eA;
address constant LOCAL_TREASURY = 0x078D888E40faAe0f32594342c85940AF3949E666;

/**
* @notice Get deployment parameters for a specific chain
* @param chainId The chain ID to get parameters for
* @return params The Parameters struct with all constructor arguments
*/
function getParams(uint256 chainId) public pure returns (Parameters memory params) {
if (chainId == SEPOLIA_CHAIN_ID) {
// Sepolia Testnet Configuration
params = Parameters({
oracleAddress: SEPOLIA_SHU_ORACLE_ADDRESS, // Will be replaced with actual address
scalingFactor: 1e24,
treasury: SEPOLIA_TREASURY,
treasuryFee: 25e20, // 0.25%
fee: 15e21, // 1.5%
thresholdSupplySc: 5e11, // 500k SC
rcMinPrice: 1e18, // 1 ETH per RC
rcInitialPrice: 1e20, // 100 ETH per RC
txLimit: 1e10 // 10k SC
});
} else {
// Local/Anvil Default Configuration
// oracleAddress = address(0) signals that a mock oracle should be deployed
params = Parameters({
oracleAddress: address(0), // Signal to deploy mock
scalingFactor: 1e24,
treasury: LOCAL_TREASURY,
treasuryFee: 0, // 0% for testing
fee: 15e21, // 1.5%
thresholdSupplySc: 1e6, // 1M SC
rcMinPrice: 1e18, // 1 ETH per RC
rcInitialPrice: 1e20, // 100 ETH per RC
txLimit: 200e6 // 200 SC
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incorrect comment: transaction limit is 200M SC, not 200 SC.

The value 200e6 equals 200,000,000 (200 million SC), not 200 SC. The comment is off by a factor of 1,000,000.

Apply this diff to correct the comment:

-                txLimit: 200e6                              // 200 SC
+                txLimit: 200e6                              // 200M SC
📝 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
txLimit: 200e6 // 200 SC
txLimit: 200e6 // 200M SC
🤖 Prompt for AI Agents
In scripts/DeploymentParametersTefnut.sol around line 66, the inline comment for
txLimit incorrectly states "200 SC" while the value 200e6 equals 200,000,000
(200 million) SC; update the comment to reflect the correct amount such as
"200,000,000 SC (200M SC)" so it accurately documents the numeric value.

});
}
}

/**
* @notice Get human-readable network name
* @param chainId The chain ID
* @return name The network name
*/
function getNetworkName(uint256 chainId) public pure returns (string memory name) {
if (chainId == SEPOLIA_CHAIN_ID) {
return "Ethereum Sepolia";
} else if (chainId == ANVIL_CHAIN_ID) {
return "Anvil Local";
} else {
return "Unknown Network";
}
}
}
88 changes: 88 additions & 0 deletions scripts/deployDjedTefnutContract.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: AEL
pragma solidity ^0.8.0;

import "forge-std/Script.sol";
import "./DeploymentParametersTefnut.sol";
import {DjedTefnut} from "../src/DjedTefnut.sol";
import {MockShuOracleTefnut} from "../src/mock/MockShuOracleTefnut.sol";

/**
* @title DeployDjedTefnut
* @notice Deployment script for DjedTefnut contract
* @dev Automatically deploys mock oracle for local/test chains
*
* Usage:
* Local (Anvil): forge script scripts/deployDjedTefnutContract.s.sol --broadcast --rpc-url http://127.0.0.1:8545
* Sepolia: forge script scripts/deployDjedTefnutContract.s.sol --broadcast --rpc-url $SEPOLIA_RPC_URL
*/
contract DeployDjedTefnut is Script, DeploymentParametersTefnut {

// Default oracle exchange rate: 1 USD = 0.5 ETH (in weis per whole SC)
uint256 constant DEFAULT_ORACLE_PRICE = 5e17;

// Initial balance to seed the contract
uint256 constant INITIAL_BALANCE = 1e18; // 1 ETH

function run() external {
// Get deployment parameters based on current chain
Parameters memory params = getParams(block.chainid);

console.log("===========================================");
console.log("Deploying DjedTefnut");
console.log("Network:", getNetworkName(block.chainid));
console.log("Chain ID:", block.chainid);
console.log("===========================================");

// Start broadcasting transactions
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

// Dynamic Mocking: Deploy mock oracle if oracleAddress is address(0)
if (params.oracleAddress == address(0)) {
console.log("");
console.log("Oracle address is zero - deploying MockShuOracleTefnut...");

MockShuOracleTefnut mockOracle = new MockShuOracleTefnut(DEFAULT_ORACLE_PRICE);
params.oracleAddress = address(mockOracle);

console.log("MockShuOracleTefnut deployed at:", address(mockOracle));
console.log("Initial oracle price:", DEFAULT_ORACLE_PRICE);
}

// Log deployment parameters
console.log("");
console.log("Deployment Parameters:");
console.log(" Oracle Address:", params.oracleAddress);
console.log(" Scaling Factor:", params.scalingFactor);
console.log(" Treasury:", params.treasury);
console.log(" Treasury Fee:", params.treasuryFee);
console.log(" Fee:", params.fee);
console.log(" Threshold Supply SC:", params.thresholdSupplySc);
console.log(" RC Min Price:", params.rcMinPrice);
console.log(" RC Initial Price:", params.rcInitialPrice);
console.log(" TX Limit:", params.txLimit);

// Deploy DjedTefnut with the (potentially updated) parameters
DjedTefnut djedTefnut = new DjedTefnut{value: INITIAL_BALANCE}(
params.oracleAddress,
params.scalingFactor,
params.treasury,
params.treasuryFee,
params.fee,
params.thresholdSupplySc,
params.rcMinPrice,
params.rcInitialPrice,
params.txLimit
);

console.log("");
console.log("===========================================");
console.log("DjedTefnut deployed at:", address(djedTefnut));
console.log("StableCoin deployed at:", address(djedTefnut.stableCoin()));
console.log("ReserveCoin deployed at:", address(djedTefnut.reserveCoin()));
console.log("Initial Reserve:", address(djedTefnut).balance);
console.log("===========================================");

vm.stopBroadcast();
}
}
218 changes: 218 additions & 0 deletions src/DjedTefnut.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// SPDX-License-Identifier: AEL
pragma solidity ^0.8.0;

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

Comment on lines +1 to +8
Copy link

@coderabbitai coderabbitai bot Dec 13, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SPDX identifier AEL may be invalid for tooling/verification

If AEL isn’t a recognized SPDX license identifier in your pipeline/tooling, you’ll get warnings or verification friction. Consider switching to a standard SPDX id (or UNLICENSED) consistent with the repo’s licensing.

🤖 Prompt for AI Agents
In src/DjedTefnut.sol around lines 1 to 8, the SPDX license identifier at the
top is set to "AEL", which may not be a recognized SPDX identifier and can
trigger tooling/verification warnings; update the SPDX header to a valid SPDX
identifier used by the repo (for example "MIT", "GPL-3.0-or-later", etc.) or use
"UNLICENSED" if the code is not intended to be published, and ensure it exactly
matches the SPDX identifier casing and syntax (SPDX-License-Identifier: <ID>).

Copy link
Author

Choose a reason for hiding this comment

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

Isnt this incorrect?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

AEL SPDX license identifier

💡 Result:

Do you mean a specific license abbreviated “AEL”? If so, what does AEL stand for (full license name or a link)? I can then look up the correct SPDX identifier.


🌐 Web query:

official SPDX license list identifiers 2025

💡 Result:

The official SPDX license identifiers are published on the SPDX License List page and in the SPDX license-list-data repository (machine-readable JSON/XML). Use the licenses page for the human-readable table and the GitHub repo (licenses.json / exceptions.json) for authoritative identifiers you can consume programmatically. Current references:

  • SPDX License List (web page, version 3.27.0, 2025). [1]
  • SPDX license-list-data (GitHub) — machine-readable files (json/xml) containing all identifiers. [2]

If you want the full JSON file of identifiers, download licenses.json from the license-list-data repo. [2]

Sources
[1] SPDX: SPDX License List — https://spdx.org/licenses/
[2] spdx/license-list-data — https://github.com/spdx/license-list-data


🏁 Script executed:

#!/bin/bash
# Fetch the official SPDX license list and check if "AEL" is a valid identifier
curl -s https://raw.githubusercontent.com/spdx/license-list-data/main/json/licenses.json | jq -r '.licenses[] | .licenseId' | grep -i "^AEL$"

Length of output: 0


@negimox, I've checked the official SPDX license list (version 3.27.0), and "AEL" is not a recognized standard SPDX license identifier. So CodeRabbit's warning is technically accurate from a standards perspective.

However, if "AEL" is a custom or proprietary license identifier specific to your project (such as "All Emcompassing License" or similar), then using it may be intentional. Could you clarify:

  1. What does "AEL" stand for in your project's context?
  2. Is this a custom license identifier you're using intentionally?

If it's a custom identifier and intentional, you can safely ignore the warning. If you meant to use a standard SPDX identifier, I can help you find the appropriate one. Alternatively, if the code is proprietary/closed-source, you might want to use UNLICENSED instead.

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 TREASURY_FEE; // fixed treasury fee (no decay)

// Djed Parameters:
uint256 public immutable FEE;
uint256 public immutable THRESHOLD_SUPPLY_SC;
uint256 public immutable RC_MIN_PRICE;
uint256 public immutable RC_INITIAL_PRICE;
uint256 public immutable TX_LIMIT;

// Scaling factors:
uint256 public immutable SCALING_FACTOR; // used to represent a decimal number `d` as the uint number `d * SCALING_FACTOR`
uint256 public immutable SC_DECIMAL_SCALING_FACTOR;
uint256 public immutable RC_DECIMAL_SCALING_FACTOR;

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 treasuryFee,
uint256 fee,
uint256 thresholdSupplySc,
uint256 rcMinPrice,
uint256 rcInitialPrice,
uint256 txLimit
) payable {
// Constructor validation
require(oracleAddress != address(0), "Invalid oracle address");
require(treasury != address(0), "Invalid treasury address");
require(scalingFactor > 0, "Scaling factor must be > 0");
require(fee + treasuryFee <= scalingFactor, "Total fees exceed 100%");
require(rcInitialPrice >= rcMinPrice, "Initial price < min price");
require(thresholdSupplySc > 0, "Threshold supply must be > 0");
require(txLimit > 0, "TX limit must be > 0");

stableCoin = new Coin("StableCoin", "SC");
reserveCoin = new Coin("ReserveCoin", "RC");
SC_DECIMAL_SCALING_FACTOR = 10 ** stableCoin.decimals();
RC_DECIMAL_SCALING_FACTOR = 10 ** reserveCoin.decimals();
SCALING_FACTOR = scalingFactor;

TREASURY = treasury;
TREASURY_FEE = treasuryFee;

FEE = fee;
THRESHOLD_SUPPLY_SC = thresholdSupplySc;
RC_MIN_PRICE = rcMinPrice;
RC_INITIAL_PRICE = rcInitialPrice;
TX_LIMIT = txLimit;

oracle = IOracleShu(oracleAddress);
oracle.acceptTermsOfService();
}

// 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) / SC_DECIMAL_SCALING_FACTOR;
}

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);
}

function E(uint256 currentPaymentAmount) external view returns (uint256) {
return E(scMaxPrice(currentPaymentAmount), currentPaymentAmount);
}

// Ratio functions kept for informational purposes only (no longer restrict transactions)
function ratio() external view returns (uint256) {
uint256 liabilities = L(scMaxPrice(0));
if (liabilities == 0) return type(uint256).max;
return SCALING_FACTOR * R(0) / liabilities;
}

// # 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);
uint256 amountSc = (amountBc * SC_DECIMAL_SCALING_FACTOR) / scP;
require(amountSc <= TX_LIMIT || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "buySC: tx limit exceeded");
require(amountSc > 0, "buySC: receiving zero SCs");
stableCoin.mint(receiver, amountSc);
// Reserve ratio check removed in Tefnut
emit BoughtStableCoins(msg.sender, receiver, amountSc, amountBc);
}

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");
uint256 scP = scMinPrice(0);
uint256 value = (amountSc * scP) / SC_DECIMAL_SCALING_FACTOR;
uint256 amountBc = deductFees(value, feeUi, ui);
require(amountBc > 0, "sellSC: receiving zero BCs");
stableCoin.burn(msg.sender, amountSc);
transferEth(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);
require(amountBc <= (TX_LIMIT * scP) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "buyRC: tx limit exceeded");
uint256 amountRc = (amountBc * RC_DECIMAL_SCALING_FACTOR) / rcBp;
require(amountRc > 0, "buyRC: receiving zero RCs");
reserveCoin.mint(receiver, amountRc);
// Reserve ratio check removed in Tefnut
emit BoughtReserveCoins(msg.sender, receiver, amountRc, 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 scP = scMaxPrice(0);
uint256 value = (amountRc * rcTargetPrice(scP, 0)) / RC_DECIMAL_SCALING_FACTOR;
require(value <= (TX_LIMIT * scP) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellRC: tx limit exceeded");
uint256 amountBc = deductFees(value, feeUi, ui);
require(amountBc > 0, "sellRC: receiving zero BCs");
reserveCoin.burn(msg.sender, amountRc);
transferEth(receiver, amountBc);
// Reserve ratio check removed in Tefnut
emit SoldReserveCoins(msg.sender, receiver, amountRc, amountBc);
}

// sellBothCoins function removed in Tefnut

// # Auxiliary Functions

function deductFees(uint256 value, uint256 feeUi, address ui) internal returns (uint256) {
require(ui != address(0), "Invalid UI address");
require(feeUi + FEE + TREASURY_FEE <= SCALING_FACTOR, "Total fees exceed 100%");
uint256 f = (value * FEE) / SCALING_FACTOR;
uint256 fUi = (value * feeUi) / SCALING_FACTOR;
uint256 fT = (value * TREASURY_FEE) / SCALING_FACTOR; // Fixed treasury fee (no decay)
transferEth(TREASURY, fT);
transferEth(ui, fUi);
// transferEth(address(this), f); // this happens implicitly, and thus `f` is effectively transferred to the reserve.
return value - f - fUi - fT; // amountBc
}

// isRatioAboveMin and isRatioBelowMax functions removed in Tefnut

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

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

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);
}

function rcTargetPrice(uint256 _scPrice, uint256 currentPaymentAmount) internal view returns (uint256) {
uint256 supplyRc = reserveCoin.totalSupply();
require(supplyRc != 0, "RC supply is zero");
require(R(currentPaymentAmount) >= L(_scPrice), "Under-collateralized: reserve < liability");
return (E(_scPrice, currentPaymentAmount) * RC_DECIMAL_SCALING_FACTOR) / supplyRc;
}

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
? RC_INITIAL_PRICE
: Math.max(rcTargetPrice(_scPrice, currentPaymentAmount), RC_MIN_PRICE);
}

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