chainLink vrf實驗

bighu發表於2024-09-13

目標

用vrf寫一個隨機紅包

資料結構

紅包:

struct Envelope {
    Type t;    // 型別,只是erc20 和eth紅包
    ERC20 token;  // erc20 ,如果是erc20紅包,這裡是erc2o的地址
    address sender;  // 發紅包的sender
    uint balance;  // 金額
    bool allowAll;  // 允許所有人領取
    uint32 maxReceiver;  // 最大領取數,eg:最多3個人領取紅包
    bool avg; // 平均主義,每個紅包的價值等於balance/maxReceiver //填false則使用隨機紅包
    uint avgMonty; // 平均金額
    uint timeOutBlocks; //超時可以回收紅包 ,也可以開啟未領取完的紅包
    address[] received;  // 已經領取過的列表
}

每個紅包中儲存紅包的資訊,可以在允許過程中傳送紅包

儲存資料:

mapping(bytes32 => Envelope) public envelopes;   // 紅包hash  -> 紅包內容,領取紅包時需要提供紅包hash
mapping(bytes32 => mapping(address => bool)) public addressAllowList;   // 紅包對應的 allowlist 這個放在紅包外面存是因為struct裡面不能放map
mapping(bytes32 => mapping(address => bool)) addressGotList;  // 已經領取的列表,與received有點重複,建議將received修改為一個int,記錄有多少人領過
mapping(uint => bytes32) openWithVRF;   // 紅包對應的vrf, 當紅包是隨即紅包時會用到這個
mapping(ERC20 => uint) ERC20Balance;    // 每個erc20對應的金額,合約自己可以透過看自己的eth餘額來判斷,erc20需要單獨記錄,應為可以同時存在多個合約,如果多個合約都是同一個erc20,需要判斷erc20的approve是否足夠
mapping(bytes32 => uint[]) VRFKey;  // vrf 對應的隨機數列表

// VRFV2PlusClient public COORDINATOR;
bytes32 keyHash = 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae;  // VRF 用到的key,可以去官方文件查

uint256 s_subscriptionId;  // vrf 用到的另一個key

uint32 immutable callbackGasLimit = 100000;  // 官方推薦配置

// The default is 3, but you can set this higher.
uint16 immutable requestConfirmations = 3;  //官方推薦配置

上面的mapping 主要是紅包合約的配置
下方的資料則是chainlink vrf的配置,這些key可以去官網檢視具體的含義

合約的初始化

    constructor(
        uint256 _subscriptionId,
        address _coordinator
    ) VRFConsumerBaseV2Plus(_coordinator){
        s_subscriptionId = _subscriptionId;
    }

初始化主要是賦值vrf的訂閱id(後續具體操作有詳細過程)

構建紅包

    function createETHredEnvelope(
        bool allowAll,
        address[] memory allowList,
        uint32 maxReceiver,
        bool avg,
        uint timeOutBlocks
    ) public payable returns (bytes32) {}
  • allowAll:執行所有人領取,如果是true那麼任何人都可以根據紅包hash呼叫get方法領取紅包
  • allowList:如果allowAll,那麼allowList無用
  • maxReceiver:最大領取數,最大領取數可以比allowList小,這樣代表有人領不到紅包
  • avg: 是否平均,如果平均那麼每個人領取到的金額 = msg.value/maxReceiver
  • timeOutBlocks:經過多少各block後超時,超時之後紅包的發起人可以回收紅包餘額,或者開啟紅包
    function createERC20redEnvelope(
        address token,
        uint size,  // erc20的數量
        bool allowAll,
        address[] memory allowList,
        uint32 maxReceiver,
        bool avg,
        uint timeOutBlocks
    ) public returns (bytes32) {}

createERC20redEnvelopecreateETHredEnvelope 的區別只是使用的是erc20

函式內的區別在與erc20要校驗有沒有足夠的apporve

        uint approved = ERC20(token).allowance(msg.sender,address(this));
        require(approved>=ERC20Balance[ERC20(token)]+size);
        ERC20Balance[ERC20(token)] += size;

新增AllowList

    function allowSome(bytes32 hash, address[] memory allowList) public {
        require(envelopes[hash].balance != 0, "envelop balance is 0");
        require(envelopes[hash].sender == msg.sender,"only envelops sender can do this");
        for (uint i = 0; i < allowList.length; i++) {
            addressAllowList[hash][allowList[i]] = true;
        }
    }

這就不多解釋了

領取紅包

    function get(bytes32 hash) public {}

領紅包的方法簽名很簡單,只需要傳一個紅包hash就可以,但內部邏輯很複雜,重點看一下它裡面的判斷

        require(envelopes[hash].balance != 0, "envelop balance is 0"); // 判讀紅包餘額不為0
        require(!addressGotList[hash][msg.sender], "has got"); // 判斷髮起人是否已經領取過
        require(   // 判斷紅包是否已經超時
            envelopes[hash].timeOutBlocks > block.number,
            "envelop timeOutBlocks is not enough"
        );
        require(
            addressAllowList[hash][msg.sender] || envelopes[hash].allowAll,// 判斷髮起人是否被允許
            "not allow"
        );
        require(envelopes[hash].received.length < envelopes[hash].maxReceiver, "no more"); // 還是判斷是否已經領取完

在領取上有兩種邏輯,一種是平均紅包,平均紅包get後會馬上到賬。一種是隨機數紅包,隨機數紅包不會立馬到賬需要等領紅包的人數達到maxReceiver 或者紅包超時,後面會詳細講怎麼領隨機數紅包。

開啟隨機數紅包

    function openEnvelopes(bytes32 hash)public{
        require(
            envelopes[hash].timeOutBlocks < block.number || envelopes[hash].received.length == envelopes[hash].maxReceiver,
            "envelop timeOutBlocks is not enough"
        );
        require(envelopes[hash].maxReceiver > 0,"max receriver max more than 0");

開啟隨機數紅包一般是在領取時自動呼叫,如果領取人沒有達到maxReciver,可以在紅包超時後手動呼叫。

這個方法中會向vrf請求一個隨機數,正常情況chainlink會呼叫fulfillRandomWords方法來返回隨機數。

    function fulfillRandomWords(
        uint256 requestId,
        uint256[] calldata randomWords
    ) internal override {
        require(randomWords.length == envelopes[openWithVRF[requestId]].received.length);
        VRFKey[openWithVRF[requestId]] = randomWords;
    }

實際上可以在這個方法裡面寫紅包分發的內容,但是由於這一步是chainlink觸發的是由他來執行手續費,所以這裡面邏輯不能太複雜(實際上限制的引數就是keyHash 這個變數)

手動開啟紅包

由於chainlink返回的時候不能有複雜的邏輯,所以隨機數紅包只能由手動觸發

    function openVRFEnvelop(bytes32 hash)public {
        uint[] memory randomWords = VRFKey[hash];
        require(envelopes[hash].maxReceiver > 0,"max receriver max more than 0");
        require(randomWords.length!=0,"can not get vrf words");
        uint16[] memory words = new uint16[](randomWords.length);
        // 計算每一個小分段的權重
     }

vrf訂閱id獲取

首先我們需要去chainlink上領取一點測試幣(link幣和eth幣,兩個都要,如有已經有了可以跳過)

網址: https://faucets.chain.link/

image-20240913182431182

然後需要去crf管理頁面構建一個錢包合約,後面請求vrf隨機數時會扣除Link幣

網址:https://vrf.chain.link/

image-20240913182732964

填完資訊後,還是這樣網址,下面會出現你的sub

image-20240913182948316

點選你的sub,裡面有sub的id,這個id就是合約部署時要用到的id,可以用這個id先把合約部署上去,後面要合約的地址

image-20240913183148412

在這個頁面的右下角找到fund 給這個sub衝點link幣

image-20240913183021680

衝完之後點左邊的add cousumer ,把你的合約地址填進來

至此,這個紅包合約就可以用了

測試

這個紅包我已經部署在測試網路上了,可以直接去上面試試

https://sepolia.etherscan.io/address/0xc81c0913e6365eb31e761d1062b41dd5a96d2e90#writeContract

合約原始碼後續會貼在這裡(今天網太卡了,我環境一直下載不下來)

原始碼地址:(這兩天環境弄好了我會把程式碼放上去,目前還是一個空專案)

https://github.com/bighu630/redEnvelop