Assignment

1.Assignment guidance

Consider a smart community of electricity prosumers (electricity consumers who also produce electricity). There can be a mismatch between energy demand of a prosumer and the produced energy during a specific time interval. When a prosumer produces more energy than its requirement, it can sell it to the other prosumers of the community who do not have enough energy to fulfill their need. Traditionally, a third party manages the trading of electricity between prosumers; however, a significant cost is paid to the third party for its services and the system is prone to security and privacy issues. To mitigate these challenges, a blockchain based energy trading mechanism emerged as a promising solution. In this coursework, you are required to design a smart contract for peer-to-peer (P2P) energy trading between electricity prosumers.

image-20230504005644870

Develop a main smart contract for energy trading for a local energy market where prosumers send requests for buying and selling energy. The smart contract should match the energy requests of buyers with sellers and trade the energy. In the smart contract, the prosumers must register before taking part in energy trading. When a new prosumer is registered on the network, a smart wallet for the prosumer is automatically created by the smart contract. To purchase the energy, a prosumer must have Ethers in his smart wallet (user can send ethers to smart contract). A registered prosumer should be able to send energy surplus (sell) or deficit (buy) request to the main smart contract: positive value for surplus energy and negative value for deficit energy.

image-20230504005656779

On getting the request, the main smart contract should check if the requester is buyer or seller and call the respective energy trading function of P2P smart contract (in case of buyer, main smart contract also checks if the buyer has sufficient balance in his smart wallet to purchase the required amount of energy, for instance, assume for 1 unit of energy, a buyer needs to pay 1 Ether). On getting the energy buying request, the P2P smart contract should first find the available energy seller in the market who has sufficient surplus energy to fulfill the energy requirement of the buyer. In case no seller is available, the buyer’s request should be added to the queue. Similarly, on getting the energy selling request, the P2P smart contract should first find the energy buyer in the market (queue) whose energy demand can be fulfilled by the seller; otherwise, the seller is added to the available seller’s queue. After energy trading, the information should be stored in the blockchain (records of both buyer and seller should be updated), and payment of energy should be added to the energy seller’s account (smart wallet). For each prosumer, its ID(address), energy status (how much energy it needs to buy or sell), and balance (Ethers in smart wallet) must be stored in the blockchain as a record and it must be updated after the prosumer takes part in energy trading. A prosumer should be able to withdraw his Ethers from smart wallet.

image-20230504005717739

Traditionally, the electricity prosumers prefer to store the surplus energy in the local energy storage system (ESS) instead of selling it to the buyers. They use this energy instead of buying energy from other sellers when they are in energy deficit. Propose (and implement in the smart contract) an incentive mechanism for the energy sellers and buyers to encourage them to take part in energy trading.

2.Assessment tasks

The following functions must be the part of the smart contracts. (The smart contracts are not limited to only these functions. You can add more functions as per requirement.)

P2P smart contract

(a) A structure to store information of prosumers. The information must include a prosumer’s ID (address), energy status (how much energy it needs to buy or sell), and balance (Ethers in smart wallet). You can also add more information as per requirements. Note that the data of buyers and sellers should not be stored separately, it should be stored in the same structure named “prosumer”. The data related to prosumers will only be stored in P2P smart contract. No more than one struct shall be used to store information.

(b) A function to register a new prosumer (add information of a new prosumer). Initially, only address of the user is added as its ID.

(c) Add functions to buy and sell energy.

Main smart contract

(a) Modifier function to make sure a prosumer is registered in the system before sending any request.

(b) Modifier function to ensure single registration of a prosumer (if an already registered prosumer request for the registration again, the function should send an error message saying the user is already registered).

(c) A modifier function to check whether a buyer has deposited sufficient funds (Ethers required to purchase the required amount of energy) to buy energy in the smart wallet.

(d) A public function to register a prosumer. A prosumer only calls this function to get registered (prosumer does not pass any value). The function checks (using the modifier function) the prior registration of the prosumers. If prosumer is already registered, the error message is generated (by the modifier function) to show that prosumer is already registered; otherwise the address of the new prosumer is sent to the P2P smart contract for storage (registration).

(e) A public function to enable a buyer to deposit some Ethers prior to energy buying request. The prosumer dose not pass any value to the function.

(f) A public function to accept prosumers’ requests and check if a prosumer has sent an energy selling or buying request and pass the data to the P2P smart contract. A prosumer passes positive value if he is a seller and negative value if he is a buyer. For example, if a buyer needs 3 units of energy, he will send -3 as input. The negative sign shows that the buyer needs the energy. On the contrary, if a seller wants to 3 units of energy, it will send 3 as an input. The positive 3 shows that the user has surplus energy to sell.

(g) A public function to check the current energy status of a seller or buyer (the amount of energy a buyer wants to buy or a seller

wants to sell). The function should not have any input arguments.

(h) A public function to check the balance of a prosumer. The function should not have any input arguments.

(i) A public function to withdraw the Ethers from smart wallets of prosumers. The function should not have any input arguments.(Note: funds can be withdrawn for a prosumer if his energy status is greater than or equal to zero, which means the prosumer does not need to buy energy at the moment.)

image-20230504005812251

Scenario to implement

Consider a scenario of a smart community with 20 electricity prosumers with unique addresses. All of the prosumers are registered on the blockchain. Suppose 10 prosumers are energy deficit and send the energy buying request to the smart contract. For each prosumer, randomly select energy deficit status (amount of required energy) between (1-6) units. On the other hand, 10 prosumers have surplus energy and choose their energy status (the amount of energy they want to sell) between (1-8) units. Send the requests of prosumers to the smart contract randomly and observe the behaviour of the system according to the instructions given above. Create a table and add the following information of all 20 prosumers in it:

(a) Address

(b) Initial energy status before energy trading

(c) Balance in the smart wallet before energy trading

(d) Energy status after energy trading

(e) Balance in smart wallet after energy trading

Incentive Mechanism

I build communities where prosumers buy and sell energy.

In order to allow prosumers who have excess energy to sell their energy instead of hoarding it themselves. There needs to be an incentive for him/her to sell his/her energy.

First of all, let me explain that the normal price of each energy in our energy community is 1ether, that is, 10^18wei. At the same time, if the transaction is successful, both parties will be charged a 5% handling fee (assuming 5% is the market price, that is to say, other energy communities are charged as well).

In order to allow more users to participate in the community, I will first add a part of my own ether as an incentive pool.

The incentive mechanism is mainly divided into three parts:

First order incentive: No handling fee is required for the first transaction.

Follow-up incentives: Every time you successfully complete a transaction (excluding the first order), it means that your credit value is more secure, so transaction security risks are reduced. Therefore, every time you complete an order, you will be charged 0.1% less handling fee, with a maximum limit of 3%, that is, you will be charged a minimum of 2% handling fee.

Luck incentive: Each user will have a 1% probability of free handling fee(not the first order); and there will be 1% eth return for buyers, and 1% eth for sellers as rewards, and the upper limit is 1ether.

Note: Although no handling fee is charged for the first transaction, it is necessary to ensure that the balance in the account is sufficient to pay the handling fee. We promise and provide channels to return eth to your smart wallet, please feel free to use it!

Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract P2P {

    // a) a structure to sore information of prosumers
    struct Prosumer {
        address pid; // address
        int energyStatus; // negative: amount of required energy; positive: the amount of energy they want to sell
        uint balance; // wei in smart wallet(10^18 wei = 1 ether)
        bool frozenBalance; // Funds are frozen if in progress
        bool completeFirstTrading; // Whether to complete the first transaction
        uint incentivePoints; // Credit(is the number of successful transactions after the non-first order)
    }

    // Quickly get the prosumer through the address
    mapping (address => Prosumer) internal prosumers;

    // Scenario to implement: Suppose 10 prosumers are energy deficit, and other 10 prosumers have surplus energy
    bool internal odd = false;

    // seed for generating random numbers
    uint256 internal seed = 5423633257245624352373;

    // Store the address of the prosumer who wants to buy energy
    address[] internal buyQueue;

    // Store the address of the prosumer who wants to sell energy
    address[] internal saleQueue;

    // Incentive mechanism: the upper limit is exempt from 3% handling fee
    uint internal incentivePointThreshold = 30;

    // Incentive mechanism: the upper limit of cashback is 1 ether
    uint internal cashBackThreshold = 10**18;

    // Incentive mechanism: normal handling fee is 5%
    uint internal perOfDandlingFee = 50;

    // Energy and Balance Modified Log
    event printTradingLog(string message, address buyer, address seller, int energyNum, uint balanceNum);
    // luck: Whether the luck incentive is activated;
    // incentiveCashBack: cashback amount; 
    // handlingFee: handling fee;
    event printOtherFeeLog(string message, address addr, bool luck, uint incentiveCashBack, uint handlingFee);
    // Logs of users in the queue
    event printQueueLog(string queueName, address addr);

    // b) register a new prosumer
    function register(address addr) external {
        prosumers[addr] = Prosumer(
            {
                pid: addr,
                energyStatus: odd == true ? random(1, 6, false) : random(1, 8, true),
                balance: 0,
                frozenBalance: false,
                completeFirstTrading: false,
                incentivePoints: 0
            }
        );

        odd = !odd;
    }

    // c) buy and sell energy(Contains multiple functions to complete)
    function trading(address buyer, address seller, int energyNum) internal {
        uint balanceChange = uint(energyNum * (10**18));
        // handling energy
        emit printTradingLog("energy status and balance start changing", buyer, seller, energyNum, balanceChange);
        modifyEnergyStatus(buyer, energyNum);
        modifyEnergyStatus(seller, -energyNum);

        // handling balance
        decBalance(buyer, balanceChange);
        incBalance(seller, balanceChange);
        emit printTradingLog("energy status and balance finish changing", buyer, seller, energyNum, balanceChange);
        
        // handling other fee
        if(getCompleteFirstTrading(buyer)) {
            otherFee(buyer, energyNum);
        }
        if(getCompleteFirstTrading(seller)) {
            otherFee(seller, energyNum);
        }
    }

    // Match buyers and sellers
    function matchRequest() external returns(int, int) {

        // Index of marked completed transactions
        int markBuyIndex = -1;
        int markSaleIndex = -1;
        for(uint i = 0; i < buyQueue.length; i++) {
            Prosumer storage buyer = prosumers[buyQueue[i]];
            for(uint j = 0; j < saleQueue.length; j++) {
                Prosumer storage seller = prosumers[saleQueue[j]];
                // Seller can fully meet the need of buyer
                if(seller.energyStatus >= -buyer.energyStatus) {
                    int transferEnergy = -buyer.energyStatus;
                    // Transactions dealing with energy and balance
                    trading(buyQueue[i], saleQueue[j], transferEnergy);

                    if(seller.energyStatus == 0) {
                        // If the energy of the seller is 0, mark it
                        markSaleIndex = int(j);
                        // Transaction completed, unfreeze
                        setFrozenBalanceOfProsumer(saleQueue[j], false);
                        setCompleteFirstTrading(saleQueue[j], true);
                    }

                    // mark buyer
                    markBuyIndex = int(i);
                    // Transaction completed, unfreeze
                    setFrozenBalanceOfProsumer(buyQueue[i], false);
                    // Transaction completed, set to true
                    setCompleteFirstTrading(buyQueue[i], true);
                    break;
                // Seller can't fully meet the need of buyer
                } else {
                    int transferEnergy = seller.energyStatus;
                    // Transactions dealing with energy and balance
                    trading(buyQueue[i], saleQueue[j], transferEnergy);

                    // mark seller
                    markSaleIndex = int(j);
                    // Transaction completed, unfreeze
                    setFrozenBalanceOfProsumer(saleQueue[j], false);
                    // Transaction completed, set to true
                    setCompleteFirstTrading(saleQueue[j], true);
                }
            }
        }

        return (markBuyIndex, markSaleIndex);
    }

    // Remove the completed matching requests from the queue
    function adjustQueue(int buyerIndex, int sellerIndex) external {
        
        // If it is -1, it means that remove is not required
        if(buyerIndex != -1) {
            uint bIndex = uint(buyerIndex);
            for(uint x = bIndex + 1; x < buyQueue.length; x++) {
                // The original location needs to be freed first, otherwise it will occupy memory
                delete buyQueue[x - bIndex - 1];
                buyQueue[x - bIndex - 1] = buyQueue[x];
            }
            for(uint x = bIndex; x >= 0; x --) {
                buyQueue.pop();
                // uint will not be less than 0, so special judgment is required
                if(x == 0) {
                    break;
                }
            }
        }

        if(sellerIndex != -1) {
            uint sIndex = uint(sellerIndex);
            for(uint y = sIndex + 1; y < saleQueue.length; y++) {
                delete saleQueue[y - sIndex - 1];
                saleQueue[y - sIndex - 1] = saleQueue[y];
            }
            for(uint y = sIndex; y >= 0; y --) {
                saleQueue.pop();
                if(y == 0) {
                    break;
                }
            }
        }
    }

    // modify energy status
    function modifyEnergyStatus(address addr, int energyNum) internal {
        prosumers[addr].energyStatus += energyNum;
    }
    
    // increase balance
    function incBalance(address addr, uint amount) internal {
        prosumers[addr].balance += amount;
    }

    // decrease balance
    function decBalance(address addr, uint amount) internal {
        prosumers[addr].balance -= amount;
    }

    // Calculate the rebate amount and cannot exceed the maximum value
    function calculateTheCashBackAmount(uint val) internal view returns (uint) {
        return cashBackThreshold >= val ? val : cashBackThreshold;
    }

    // Whether to trigger the luck incentive mechanism
    function luckIncentive() internal view returns (bool) {
        return random(1, 100, true) == 1 ? true : false;
    }

    // Calculate handling fees
    function getHandlingFee(address addr, int energyNum) public view returns (uint) {
        return uint(uint(energyNum) * (perOfDandlingFee - prosumers[addr].incentivePoints) * (10**15));//10^18/1000
    }

    // Settlement of all fees except for normal energy transaction payments
    function otherFee(address addr, int energyNum) internal {
        
        if(luckIncentive()) {
            uint incentiveCashBack = calculateTheCashBackAmount(uint(energyNum) * (10**16));//10^18/100
            incBalance(addr, incentiveCashBack);
            emit printOtherFeeLog("otherFee", addr, true, incentiveCashBack, 0);
        } else {
            uint handlingFee = getHandlingFee(addr, energyNum);
            decBalance(addr, handlingFee);
            emit printOtherFeeLog("otherFee", addr, false, 0, handlingFee);
        }

        incIncentivePointsOfProsumer(addr);
    }

    // Add to the buyer or seller queue based on Boolean values
    function queuing(address addr, bool buyerQueue) external {
        if(buyerQueue) {
            buyQueue.push(addr);
            emit printQueueLog("buyer", addr);
        } else {
            saleQueue.push(addr);
            emit printQueueLog("seller", addr);
        }
        setFrozenBalanceOfProsumer(addr, true);
    }

    // get set
    function getCompleteFirstTrading(address addr) internal view returns (bool) {
        return getProsumer(addr).completeFirstTrading;
    }

    function setCompleteFirstTrading(address addr, bool flag) internal view {
        getProsumer(addr).completeFirstTrading = flag;
    }

    // get set
    function getFrozenBalanceOfProsumer(address addr) external view returns (bool) {
        return getProsumer(addr).frozenBalance;
    }

    function setFrozenBalanceOfProsumer(address addr, bool state) internal {
        prosumers[addr].frozenBalance = state;
    }
    
    // get
    function getProsumer(address addr) internal view returns(Prosumer memory) {
        return prosumers[addr];
    }

    function getEnergyStatusOfProsumer(address addr) external view returns (int) {
        return getProsumer(addr).energyStatus;
    }

    function getBalanceOfProsumer(address addr) external view returns (uint) {
        return getProsumer(addr).balance;
    }

    // get inc
    function getIncentivePointsOfProsumer(address addr) external view returns (uint) {
        return getProsumer(addr).incentivePoints;
    }

    function incIncentivePointsOfProsumer(address addr) internal {
        if(incentivePointThreshold > prosumers[addr].incentivePoints) {
            prosumers[addr].incentivePoints += 1;
        }
        
    }

    // modify balance by wei(10^18wei=1ether)
    function incBalanceByEth(address addr, uint256 value) public payable {
        prosumers[addr].balance += value;
    }

    function decBalanceByEth(address addr, uint256 amount) public {
        prosumers[addr].balance -= amount;
    }
    
    function regitered(address addr) external view returns(bool) {
        if(prosumers[addr].pid == address(0x0)) {
            // The Prosumer object corresponding to this address has not been created yet
            return false;
        } else {
            return true;
        }
    }

    // Generate random numbers according to the interval, and you can specify whether they are negative or positive
    function random(uint256 from, uint256 to, bool positive) internal view returns (int256) {
        require(from < to, "The interval size should be a positive number!");

        uint256 randomNumber = uint256(keccak256(abi.encodePacked(seed, block.timestamp)));

        uint256 range = to - from + 1;
        int256 randomRangeNumber = int256((randomNumber % range) + from);
        
        return positive == true ? randomRangeNumber : -randomRangeNumber;
    }
}

contract ElecCommunity {

    // main smart contract interacts with p2p smart contract.
    P2P internal p2p;

    // Create p2p first, and then pass in the contract address
    constructor(address addr) {
        p2p = P2P(addr);
    }

    // function call log
    event printCallingFuncLog(string contractName, string funcCalling, address sender);
    // ether change log
    event printEtherChangeLog(address sender, string operation, uint amount);

    // a) make sure a prosumer is registered
    modifier haveRegistered {
        require(p2p.regitered(msg.sender), "Need to register first!");
        
        _;
    }

    // b) ensure single registration of a prosumer
    modifier unregistered {
        require(!p2p.regitered(msg.sender), "Already registered!");
        
        _;
    }

    // c) check whether a buyer has deposited sufficient funds
    modifier checkBuyerDeposit(address addr) {
        require(p2p.getBalanceOfProsumer(addr) >= uint(-p2p.getEnergyStatusOfProsumer(addr) * 10**18) + p2p.getHandlingFee(addr, -p2p.getEnergyStatusOfProsumer(addr)), "Balance isn't enough to purchase energy!");

        _;
    }

    // d) register a prosumer
    function register() public unregistered {
        emit printCallingFuncLog("ElecCommunity", "start calling function register", msg.sender);
        p2p.register(msg.sender);
        emit printCallingFuncLog("ElecCommunity", "finish calling function register", msg.sender);
    }

    // e) deposit some ethers
    function addEthers() public payable haveRegistered {  
        emit printEtherChangeLog(msg.sender, "begin adding", msg.value);
        p2p.incBalanceByEth(msg.sender, msg.value);
        emit printEtherChangeLog(msg.sender, "end adding", msg.value);
    }

    // f) accept prosumers' requests
    function tradingRequest() public haveRegistered {
        emit printCallingFuncLog("ElecCommunity", "start calling function tradingRequest", msg.sender);
        // join the queue
        addQueue(msg.sender);
        // matching
        (int buyerIndex, int sellerIndex) = p2p.matchRequest();
        // Remove users from queue who have completed transactions
        p2p.adjustQueue(buyerIndex, sellerIndex);

        emit printCallingFuncLog("ElecCommunity", "finish calling function tradingRequest", msg.sender);
    }

    // g) check the current energy status of a seller of buyer
    function isBuyer(address addr) internal view returns (bool) {
        return p2p.getEnergyStatusOfProsumer(addr) > 0 ? false : true;
    }

    // h) check balance of a prosumer
    function checkBlanceBefWithdraw(address addr, uint256 amount) internal view returns (bool) {
        return p2p.getBalanceOfProsumer(addr) >= amount ? true : false;
    }

    // i) withdraw the wei(10^18=1eth) from smart wallets of prosumers
    function withdrawEthers(uint256 amount) public haveRegistered needActiveState(msg.sender) {
        require(checkBlanceBefWithdraw(msg.sender, amount), "Not enough balance!");
        emit printEtherChangeLog(msg.sender, "begin withdrawing", amount);
        p2p.decBalanceByEth(msg.sender, amount);
        payable(msg.sender).transfer(amount);
        emit printEtherChangeLog(msg.sender, "end withdrawing", amount);
    }

    // Prevent funds from being withdrawn when trading
    modifier needActiveState(address addr) {
        require(!p2p.getFrozenBalanceOfProsumer(addr), "You can do this operation after trading!");

        _;
    }

    // join buyer queue
    function addBuyerQueue(address addr) internal checkBuyerDeposit(addr) {
        p2p.queuing(addr, true);
    }

    // join seller queue
    function addSellerQueue(address addr) internal checkSellerDeposit(addr) {
        p2p.queuing(addr, false);
    }

    // check seller's deposit
    modifier checkSellerDeposit(address addr) {
        require(p2p.getBalanceOfProsumer(addr) >= p2p.getHandlingFee(addr, p2p.getEnergyStatusOfProsumer(addr)), "Balance isn't enough to pay handling fee!");

        _;
    }

    // judge and join queue
    function addQueue(address addr) internal needActiveState(addr) {
        bool buyerFlag = isBuyer(addr);
        buyerFlag == true ? addBuyerQueue(addr) : addSellerQueue(addr);
    }
}

Table of system behavior

Address Energy(before) Balance(before) Energy(after) Balance(after)
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 1 1 0 2
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 -4 6 0 2
0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db 6 2 0 8
0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB -6 7 0 1
0x617F2E2fD72FD9D5503197092aC168c91465E7f2 3 2 0 5
0x17F6AD8Ef982297579C203069C1DbfFE4348c372 -5 8 0 3
0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678 5 3 0 8
0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7 -3 9 0 6
0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C 7 1 0 8
0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC -5 6 0 1
0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c 3 4 0 7
0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C -5 7 0 2
0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB 7 2 0 9
0x583031D1113aD414F02576BD6afaBfb302140225 -3 6 0 3
0xdD870fA1b7C4700F2BD7f44238821C26f7392148 4 1 0 5
0xFedA05F598803923A2Dd114483E57a1C423353E5 -5 6 0 1
0x2651d6bE063936368022c7C54608716e2d843333 3 2 0 5
0xA1D603FD36eA19d374774c0bD81c88B5Dd07C6af -4 8 0 4
0x49a04437F0154F02536fF7000fB4bcf679487DA6 7 4 0 11
0x6A47F9a7b058EBbee1F8B8F42766F5A4d66aE1Fd -6 9 0 3

Note: In fact, when I first got this result, I felt a bit unexpected, but after checking and calculating, there is indeed no problem. The energy of buyers and sellers in this experiment just offset (The absolute value of both sides is 46).

Improvements

In fact, I didn’t add too many new features to smart contract, most of the features not mentioned in the documentation are authored for the incentive mechanism, which has already been explained in the first point. In addition, there are some new functions, such as random functions, which are used to generate random values, and some getter and setter functions are used to obtain the information of fields in prosumer, and I also added some event functions to the smart contract to print and track logs. Finally, each of my functions has comments for easy understanding.