=== .env.example === CHAIN_RPC=https://eth-passet-hub-paseo.dotters.network PRIVATE_KEY=5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133 CONTROLLER_ADDRESS= IMPLEMENTATION_ADDRESS= === Cargo.toml === [package] name = "ibp-contracts" version = "0.1.0" edition = "2021" [dependencies] uapi = { package = "pallet-revive-uapi", version = "0.7", default-features = false } polkavm-derive = "0.29.0" [[bin]] name = "controller" path = "src/main.rs" [[bin]] name = "implementation" path = "src/implementation.rs" [profile.release] opt-level = "z" lto = true panic = "abort" === IIBP.sol === // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; interface IIBPProxy { // Events event UpgradeProposed(address indexed proposer, address indexed newImplementation, uint256 timestamp); event UpgradeVoted(address indexed voter, bool support, uint16 weight); event UpgradeExecuted(address indexed oldImplementation, address indexed newImplementation); event TemplarRemoved(address indexed templar); // Proxy functions function proposeUpgrade(address newImplementation) external; function voteUpgrade(bool support) external returns (uint16 totalWeight); function executeUpgrade() external; function getImplementation() external view returns (address); function getPendingUpgrade() external view returns (address); function removeTemplar() external; } interface IIBPImplementation { // Events event NetworkCreated(uint32 indexed networkId, address indexed creator, uint8 levelRequirement); event NetworkDnsUpdated(uint32 indexed networkId, uint8 indexed orgId, bool enabled); event PylonAdded(uint32 indexed networkId, address indexed pylon); event ProposalCreated(uint32 indexed proposalId, address indexed proposer, uint8 proposalType); event ProposalVoted(uint32 indexed proposalId, address indexed voter, bool support, uint16 weight); event ProposalExecuted(uint32 indexed proposalId, address indexed executor); event PylonLevelChanged(address indexed pylon, uint8 oldLevel, uint8 newLevel); event PylonOrgChanged(address indexed pylon, uint8 orgId); event DnsControllerChanged(uint8 indexed orgId, address indexed controller); event ProbeWhitelisted(address indexed probe); event ProbeRevoked(address indexed probe); event ProbeDataReported(address indexed pylon, address indexed probe, uint32 window, bytes32 reportHash, uint8 statusCode); event WindowFinalized(address indexed pylon, uint32 indexed window, uint8 status, uint8 totalCount); // Bootstrap function bootstrap() external; // Network management function createNetwork() external returns (uint32 networkId); function setNetworkDns(uint32 networkId, uint8 orgId, bool enabled) external; function addPylon(uint32 networkId, address pylon) external; function removePylon(uint32 networkId, address pylon) external; // Pylon management function setPylonOrg(address pylon, uint8 orgId) external returns (uint32 proposalId); function setPylonLevel(address pylon, uint8 level) external returns (uint32 proposalId); // DNS controller management function setDnsController(uint8 orgId, address controller) external returns (uint32 proposalId); function getDnsController(uint8 orgId) external view returns (address); // Probe management function whitelistProbe(address probe) external returns (uint32 proposalId); function revokeProbe(address probe) external returns (uint32 proposalId); function isProbeWhitelisted(address probe) external view returns (bool); // Governance function propose(uint8 proposalType) external returns (uint32 proposalId); function vote(uint32 proposalId, bool support) external returns (uint16 weight); function executeProposal(uint32 proposalId) external; // Query functions function getNetworkInfo(uint32 networkId) external view returns ( uint8 levelRequirement, bool ibpDnsEnabled, bool dottersDnsEnabled ); function getNetworkCount() external view returns (uint32); function getProposal(uint32 proposalId) external view returns ( uint8 proposalType, address proposer ); function getPylonLevel(address pylon) external view returns (uint8); function getPylonStatus(address pylon) external view returns (uint8); function getPylonMetrics(address pylon) external view returns ( uint8 status, uint16 avgLatency, uint8 totalCount, uint8 healthyCount, uint32 window ); // Monitoring function reportProbeData( address pylon, bytes32 reportHash, uint8 statusCode ) external returns (uint32 window); function finalizeWindow(address pylon, uint32 window) external returns (uint8 status); } // Combined interface for easier interaction interface IIBP is IIBPProxy, IIBPImplementation {} === LICENSE === Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. === Makefile === # IBP Contracts Makefile -include .env export .PHONY: all build clean deploy bootstrap test help all: build build: @echo "Building contracts..." RUSTC_BOOTSTRAP=1 cargo build --release polkatool link --strip --output controller.polkavm \ target/riscv64emac-unknown-none-polkavm/release/controller polkatool link --strip --output implementation.polkavm \ target/riscv64emac-unknown-none-polkavm/release/implementation clean: cargo clean rm -f *.polkavm deploy: build @test -n "$(CHAIN_RPC)" || (echo "Set CHAIN_RPC in .env" && exit 1) @test -n "$(PRIVATE_KEY)" || (echo "Set PRIVATE_KEY in .env" && exit 1) @echo "Deploying to $(CHAIN_RPC)..." @echo "Deploying implementation first..." $(eval IMPL_ADDR := $(shell cast send --private-key $(PRIVATE_KEY) \ --rpc-url $(CHAIN_RPC) --create \ "0x$$(xxd -p -c 99999 implementation.polkavm)" \ --json | jq -r .contractAddress)) @echo "Implementation deployed at: $(IMPL_ADDR)" @echo "Deploying controller with implementation address appended..." $(eval IMPL_BYTES := $(shell echo $(IMPL_ADDR) | sed 's/0x//')) $(eval CTRL_ADDR := $(shell cast send --private-key $(PRIVATE_KEY) \ --rpc-url $(CHAIN_RPC) --create \ "0x$$(xxd -p -c 99999 controller.polkavm)$$(echo $(IMPL_BYTES))" \ --json | jq -r .contractAddress)) @echo "Controller deployed at: $(CTRL_ADDR)" @echo "Updating .env with addresses..." @sed -i '/^CONTROLLER_ADDRESS=/d' .env 2>/dev/null || true @sed -i '/^IMPLEMENTATION_ADDRESS=/d' .env 2>/dev/null || true @echo "CONTROLLER_ADDRESS=$(CTRL_ADDR)" >> .env @echo "IMPLEMENTATION_ADDRESS=$(IMPL_ADDR)" >> .env @echo "" @echo "Deployment complete! Run 'make bootstrap' to set deployer as level 5." bootstrap: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Deploy first with 'make deploy'" && exit 1) @echo "Bootstrapping: setting deployer as level 5..." cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "bootstrap()" @echo "Bootstrap complete! Deployer is now level 5." # Network management create-network: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS in .env" && exit 1) @echo "Creating new network..." cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "createNetwork()" add-pylon: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(NETWORK_ID)" || (echo "Set NETWORK_ID" && exit 1) @test -n "$(PYLON_ADDRESS)" || (echo "Set PYLON_ADDRESS" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "addPylon(uint32,address)" $(NETWORK_ID) $(PYLON_ADDRESS) set-network-dns: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(NETWORK_ID)" || (echo "Set NETWORK_ID" && exit 1) @test -n "$(ORG_ID)" || (echo "Set ORG_ID (1=IBP, 2=dotters)" && exit 1) @test -n "$(ENABLED)" || (echo "Set ENABLED=true/false" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "setNetworkDns(uint32,uint8,bool)" $(NETWORK_ID) $(ORG_ID) $(ENABLED) # Governance proposals propose-pylon-level: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PYLON_ADDRESS)" || (echo "Set PYLON_ADDRESS" && exit 1) @test -n "$(LEVEL)" || (echo "Set LEVEL (0-9)" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "setPylonLevel(address,uint8)" $(PYLON_ADDRESS) $(LEVEL) propose-pylon-org: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PYLON_ADDRESS)" || (echo "Set PYLON_ADDRESS" && exit 1) @test -n "$(ORG_ID)" || (echo "Set ORG_ID (1=IBP, 2=dotters)" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "setPylonOrg(address,uint8)" $(PYLON_ADDRESS) $(ORG_ID) propose-dns-controller: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(ORG_ID)" || (echo "Set ORG_ID (1=IBP, 2=dotters)" && exit 1) @test -n "$(DNS_CONTROLLER)" || (echo "Set DNS_CONTROLLER address" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "setDnsController(uint8,address)" $(ORG_ID) $(DNS_CONTROLLER) whitelist-probe: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PROBE_ADDRESS)" || (echo "Set PROBE_ADDRESS" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "whitelistProbe(address)" $(PROBE_ADDRESS) revoke-probe: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PROBE_ADDRESS)" || (echo "Set PROBE_ADDRESS" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "revokeProbe(address)" $(PROBE_ADDRESS) # Voting vote: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PROPOSAL_ID)" || (echo "Set PROPOSAL_ID" && exit 1) @test -n "$(SUPPORT)" || (echo "Set SUPPORT=true/false" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "vote(uint32,bool)" $(PROPOSAL_ID) $(SUPPORT) execute-proposal: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PROPOSAL_ID)" || (echo "Set PROPOSAL_ID" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "executeProposal(uint32)" $(PROPOSAL_ID) # Upgrade management propose-upgrade: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(NEW_IMPL_ADDRESS)" || (echo "Set NEW_IMPL_ADDRESS" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "proposeUpgrade(address)" $(NEW_IMPL_ADDRESS) vote-upgrade: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(SUPPORT)" || (echo "Set SUPPORT=true/false" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "voteUpgrade(bool)" $(SUPPORT) execute-upgrade: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "executeUpgrade()" remove-templar: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "removeTemplar()" # Monitoring report-probe: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PYLON_ADDRESS)" || (echo "Set PYLON_ADDRESS" && exit 1) @test -n "$(REPORT_HASH)" || (echo "Set REPORT_HASH (bytes32)" && exit 1) @test -n "$(STATUS_CODE)" || (echo "Set STATUS_CODE (0-254)" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "reportProbeData(address,bytes32,uint8)" \ $(PYLON_ADDRESS) $(REPORT_HASH) $(STATUS_CODE) finalize-window: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PYLON_ADDRESS)" || (echo "Set PYLON_ADDRESS" && exit 1) @test -n "$(WINDOW)" || (echo "Set WINDOW number" && exit 1) cast send --private-key $(PRIVATE_KEY) --rpc-url $(CHAIN_RPC) \ $(CONTROLLER_ADDRESS) "finalizeWindow(address,uint32)" $(PYLON_ADDRESS) $(WINDOW) # Query functions get-implementation: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) cast call $(CONTROLLER_ADDRESS) "getImplementation()" --rpc-url $(CHAIN_RPC) get-pending-upgrade: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) cast call $(CONTROLLER_ADDRESS) "getPendingUpgrade()" --rpc-url $(CHAIN_RPC) get-pylon-level: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PYLON_ADDRESS)" || (echo "Set PYLON_ADDRESS" && exit 1) cast call $(CONTROLLER_ADDRESS) "getPylonLevel(address)" $(PYLON_ADDRESS) --rpc-url $(CHAIN_RPC) get-pylon-status: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(PYLON_ADDRESS)" || (echo "Set PYLON_ADDRESS" && exit 1) cast call $(CONTROLLER_ADDRESS) "getPylonStatus(address)" $(PYLON_ADDRESS) --rpc-url $(CHAIN_RPC) get-network-info: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @test -n "$(NETWORK_ID)" || (echo "Set NETWORK_ID" && exit 1) cast call $(CONTROLLER_ADDRESS) "getNetworkInfo(uint32)" $(NETWORK_ID) --rpc-url $(CHAIN_RPC) get-network-count: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) cast call $(CONTROLLER_ADDRESS) "getNetworkCount()" --rpc-url $(CHAIN_RPC) monitor: @test -n "$(CONTROLLER_ADDRESS)" || (echo "Set CONTROLLER_ADDRESS" && exit 1) @echo "Monitoring $(CONTROLLER_ADDRESS)..." @while true; do \ echo "=== $$(date) ==="; \ echo "Implementation: $$(cast call $(CONTROLLER_ADDRESS) 'getImplementation()' --rpc-url $(CHAIN_RPC))"; \ echo "Pending upgrade: $$(cast call $(CONTROLLER_ADDRESS) 'getPendingUpgrade()' --rpc-url $(CHAIN_RPC))"; \ echo "Network count: $$(cast call $(CONTROLLER_ADDRESS) 'getNetworkCount()' --rpc-url $(CHAIN_RPC))"; \ sleep 5; \ done help: @echo "IBP Contracts Management" @echo "" @echo "DEPLOYMENT:" @echo " make deploy - Deploy contracts" @echo " make bootstrap - Set deployer as level 5" @echo "" @echo "NETWORK MANAGEMENT:" @echo " make create-network - Create new network" @echo " make add-pylon NETWORK_ID=1 PYLON_ADDRESS=0x..." @echo " make set-network-dns NETWORK_ID=1 ORG_ID=1 ENABLED=true" @echo "" @echo "GOVERNANCE:" @echo " make propose-pylon-level PYLON_ADDRESS=0x... LEVEL=6" @echo " make propose-pylon-org PYLON_ADDRESS=0x... ORG_ID=1" @echo " make propose-dns-controller ORG_ID=1 DNS_CONTROLLER=0x..." @echo " make whitelist-probe PROBE_ADDRESS=0x..." @echo " make vote PROPOSAL_ID=1 SUPPORT=true" @echo " make execute-proposal PROPOSAL_ID=1" @echo "" @echo "UPGRADES:" @echo " make propose-upgrade NEW_IMPL_ADDRESS=0x..." @echo " make vote-upgrade SUPPORT=true" @echo " make execute-upgrade" @echo " make remove-templar" @echo "" @echo "MONITORING:" @echo " make report-probe PYLON_ADDRESS=0x... REPORT_HASH=0x... STATUS_CODE=0" @echo " make finalize-window PYLON_ADDRESS=0x... WINDOW=12345" @echo " make monitor" @echo "" @echo "QUERIES:" @echo " make get-implementation" @echo " make get-pylon-level PYLON_ADDRESS=0x..." @echo " make get-pylon-status PYLON_ADDRESS=0x..." @echo " make get-network-info NETWORK_ID=1" === README.md === # aiur smart contract system for the Infrastructure Builders' Program (IBP) - handles network governance, member management, and decentralized monitoring with on-chain SLA tracking. ## architecture - **controller**: upgradeable proxy, handles delegation and upgrade governance - **implementation**: core IBP logic - networks, pylons, proposals, monitoring deployed atomically with implementation address appended to controller bytecode. no separate initialization step. ## storage layout ``` 0x10: pylon levels (shared) 0x20-0x40: implementation storage 0xA1-0xA8: controller storage (upgrades) ``` ## deployment ```bash # build make build # deploy both contracts make deploy # set deployer as level 5 make bootstrap ``` contracts require funding (1 NATIVE_TOKEN default) to prevent storage write traps. ## core operations ### networks ```bash # create network (level 5+ required) make create-network # add pylon to network make add-pylon NETWORK_ID=1 PYLON_ADDRESS=0x... # configure DNS make set-network-dns NETWORK_ID=1 ORG_ID=1 ENABLED=true ``` ### governance ```bash # propose changes (level 5+) make propose-pylon-level PYLON_ADDRESS=0x... LEVEL=6 make whitelist-probe PROBE_ADDRESS=0x... # vote (level 5-7, equal weight) make vote PROPOSAL_ID=1 SUPPORT=true # execute (2/3 majority required) make execute-proposal PROPOSAL_ID=1 ``` ### monitoring ```bash # probe reports (every 5 min) make report-probe PYLON_ADDRESS=0x... REPORT_HASH=0x... STATUS_CODE=0 # finalize window (requires 3+ reports) make finalize-window PYLON_ADDRESS=0x... WINDOW=12345 ``` status codes: - 0-127: healthy - 128-199: degraded - 200-254: error states - 255: reserved (rejected) ### upgrades ```bash # propose new implementation make propose-upgrade NEW_IMPL_ADDRESS=0x... # vote make vote-upgrade SUPPORT=true # execute (48h timelock, templar can bypass) make execute-upgrade # remove templar privilege make remove-templar ``` ## query functions ```bash make get-implementation make get-pylon-level PYLON_ADDRESS=0x... make get-pylon-status PYLON_ADDRESS=0x... make get-network-info NETWORK_ID=1 ``` ## consensus math uses ceiling division for 2/3 majority: `(total * 2 + 2) / 3` - 3 votes → need 2 - 4 votes → need 3 - 5 votes → need 4 ## security - collision-resistant storage keys via keccak256 - one report per probe per window - versioned voting keys prevent cross-proposal replay - zero address and code size validation - 48h upgrade timelock (except templar) ## license Apache-2.0 === rust-toolchain.toml === [toolchain] channel = "nightly" === src/implementation.rs === #![feature(alloc_error_handler)] #![no_main] #![no_std] #![allow(static_mut_refs)] use uapi::{HostFn, HostFnImpl as api, ReturnFlags, StorageFlags}; #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { unsafe { core::arch::asm!("unimp"); core::hint::unreachable_unchecked(); } } // minimal bump allocator for `alloc` mod alloc_support { use core::{ alloc::{GlobalAlloc, Layout}, sync::atomic::{AtomicUsize, Ordering}, }; pub struct BumpAllocator { offset: AtomicUsize, } const HEAP_SIZE: usize = 64 * 1024; #[link_section = ".bss.heap"] static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE]; unsafe impl GlobalAlloc for BumpAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { let align = layout.align().max(8); let size = layout.size(); let mut offset = self.offset.load(Ordering::Relaxed); loop { let aligned = (offset + align - 1) & !(align - 1); if aligned + size > HEAP_SIZE { return core::ptr::null_mut(); } match self.offset.compare_exchange_weak( offset, aligned + size, Ordering::SeqCst, Ordering::Relaxed, ) { Ok(_) => { let heap_ptr = HEAP.as_ptr() as *mut u8; return heap_ptr.add(aligned); }, Err(o) => offset = o, } } } unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} } #[global_allocator] static GLOBAL: BumpAllocator = BumpAllocator { offset: AtomicUsize::new(0) }; #[alloc_error_handler] fn alloc_error(_layout: Layout) -> ! { unsafe { core::arch::asm!("unimp"); core::hint::unreachable_unchecked(); } } } // tiny adapters for storage apis #[inline(always)] fn sset(key: &[u8], value: &[u8]) { let _ = api::set_storage(StorageFlags::empty(), key, value); } #[inline(always)] fn sget(key: &[u8], out: &mut [u8]) { let mut slice = &mut out[..]; let _ = api::get_storage(StorageFlags::empty(), key, &mut slice); } // event emission helpers fn emit_network_created(network_id: u32, creator: &[u8; 20], level: u8) { let mut topic0 = [0u8; 32]; api::hash_keccak_256(b"NetworkCreated(uint32,address,uint8)", &mut topic0); let mut topic1 = [0u8; 32]; topic1[28..32].copy_from_slice(&network_id.to_be_bytes()); let mut topic2 = [0u8; 32]; topic2[12..32].copy_from_slice(creator); let mut data = [0u8; 32]; data[31] = level; let topics = [topic0, topic1, topic2]; api::deposit_event(&topics, &data); } fn emit_proposal_created(proposal_id: u32, proposer: &[u8; 20], ptype: u8) { let mut topic0 = [0u8; 32]; api::hash_keccak_256(b"ProposalCreated(uint32,address,uint8)", &mut topic0); let mut topic1 = [0u8; 32]; topic1[28..32].copy_from_slice(&proposal_id.to_be_bytes()); let mut topic2 = [0u8; 32]; topic2[12..32].copy_from_slice(proposer); let mut data = [0u8; 32]; data[31] = ptype; let topics = [topic0, topic1, topic2]; api::deposit_event(&topics, &data); } fn emit_proposal_voted(proposal_id: u32, voter: &[u8; 20], support: bool, weight: u8) { let mut topic0 = [0u8; 32]; api::hash_keccak_256(b"ProposalVoted(uint32,address,bool,uint16)", &mut topic0); let mut topic1 = [0u8; 32]; topic1[28..32].copy_from_slice(&proposal_id.to_be_bytes()); let mut topic2 = [0u8; 32]; topic2[12..32].copy_from_slice(voter); let mut data = [0u8; 64]; data[31] = support as u8; data[62..64].copy_from_slice(&(weight as u16).to_be_bytes()); let topics = [topic0, topic1, topic2]; api::deposit_event(&topics, &data); } fn emit_proposal_executed(proposal_id: u32, executor: &[u8; 20]) { let mut topic0 = [0u8; 32]; api::hash_keccak_256(b"ProposalExecuted(uint32,address)", &mut topic0); let mut topic1 = [0u8; 32]; topic1[28..32].copy_from_slice(&proposal_id.to_be_bytes()); let mut topic2 = [0u8; 32]; topic2[12..32].copy_from_slice(executor); let topics = [topic0, topic1, topic2]; api::deposit_event(&topics, &[]); } fn emit_window_finalized(pylon: &[u8; 20], window: u32, status: u8, total: u8) { let mut topic0 = [0u8; 32]; api::hash_keccak_256(b"WindowFinalized(address,uint32,uint8,uint8)", &mut topic0); let mut topic1 = [0u8; 32]; topic1[12..32].copy_from_slice(pylon); let mut topic2 = [0u8; 32]; topic2[28..32].copy_from_slice(&window.to_be_bytes()); let mut data = [0u8; 64]; data[31] = status; data[63] = total; let topics = [topic0, topic1, topic2]; api::deposit_event(&topics, &data); } // ---- selectors ---- #[inline(always)] fn sel(signature: &str) -> [u8; 4] { let mut h = [0u8; 32]; api::hash_keccak_256(signature.as_bytes(), &mut h); [h[0], h[1], h[2], h[3]] } // ---- storage layout ---- // shared with controller: same prefix for pylon levels const PYLON_LEVEL_PREFIX: u8 = 0x10; // implementation-only keys and prefixes const NETWORK_COUNT_KEY: [u8; 32] = [0x20; 32]; const PROPOSAL_COUNT_KEY: [u8; 32] = [0x21; 32]; const NETWORK_PREFIX: u8 = 0x30; const PYLON_PREFIX: u8 = 0x31; const DNS_PREFIX: u8 = 0x32; const ORG_PREFIX: u8 = 0x33; const PYLON_ORG_PREFIX: u8 = 0x34; const DNS_CONTROLLER_PREFIX: u8 = 0x35; const PROPOSAL_PREFIX: u8 = 0x36; const VOTE_PREFIX: u8 = 0x37; const VOTE_WEIGHT_PREFIX: u8 = 0x38; const PROBE_WINDOW_PREFIX: u8 = 0x39; const PROBE_REPORT_PREFIX: u8 = 0x3A; const PYLON_STATUS_PREFIX: u8 = 0x3B; const PROPOSAL_DATA_PREFIX: u8 = 0x3C; const PROBE_WHITELIST_PREFIX: u8 = 0x3D; const PYLON_METRICS_PREFIX: u8 = 0x3E; const WINDOW_FINALIZED_PREFIX: u8 = 0x3F; const PROPOSAL_EXECUTED_PREFIX: u8 = 0x40; // monitoring constants const REPORT_INTERVAL: u64 = 300; const MIN_PROBES_FOR_CONSENSUS: u8 = 3; // ---- key helpers ---- #[inline(always)] fn pylon_level_key(pylon: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PYLON_LEVEL_PREFIX; key[1..21].copy_from_slice(pylon); key } #[inline(always)] fn network_key(id: u32) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = NETWORK_PREFIX; key[1..5].copy_from_slice(&id.to_le_bytes()); key } #[inline(always)] fn pylon_key(network_id: u32, pylon: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PYLON_PREFIX; key[1..5].copy_from_slice(&network_id.to_le_bytes()); key[5..25].copy_from_slice(pylon); key } #[inline(always)] fn dns_key(network_id: u32, org_id: u8) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = DNS_PREFIX; key[1..5].copy_from_slice(&network_id.to_le_bytes()); key[5] = org_id; key } #[inline(always)] fn org_key(org_id: u8) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = ORG_PREFIX; key[1] = org_id; key } #[inline(always)] fn pylon_org_key(pylon: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PYLON_ORG_PREFIX; key[1..21].copy_from_slice(pylon); key } #[inline(always)] fn dns_controller_key(org_id: u8) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = DNS_CONTROLLER_PREFIX; key[1] = org_id; key } #[inline(always)] fn proposal_key(id: u32) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PROPOSAL_PREFIX; key[1..5].copy_from_slice(&id.to_le_bytes()); key } #[inline(always)] fn proposal_executed_key(id: u32) -> [u8; 32] { let mut k = [0u8; 32]; k[0] = PROPOSAL_EXECUTED_PREFIX; k[1..5].copy_from_slice(&id.to_le_bytes()); k } #[inline(always)] fn proposal_data_key(id: u32) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PROPOSAL_DATA_PREFIX; key[1..5].copy_from_slice(&id.to_le_bytes()); key } #[inline(always)] fn vote_key(proposal_id: u32, voter: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = VOTE_PREFIX; key[1..5].copy_from_slice(&proposal_id.to_le_bytes()); key[5..25].copy_from_slice(voter); key } #[inline(always)] fn vote_weight_key(proposal_id: u32, support: bool) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = VOTE_WEIGHT_PREFIX; key[1..5].copy_from_slice(&proposal_id.to_le_bytes()); key[5] = support as u8; key } #[inline(always)] fn probe_window_key(pylon: &[u8; 20], window: u32) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PROBE_WINDOW_PREFIX; key[1..21].copy_from_slice(pylon); key[21..25].copy_from_slice(&window.to_le_bytes()); key } #[inline(always)] fn window_finalized_key(pylon: &[u8; 20], window: u32) -> [u8; 32] { let mut k = [0u8; 32]; k[0] = WINDOW_FINALIZED_PREFIX; k[1..21].copy_from_slice(pylon); k[21..25].copy_from_slice(&window.to_le_bytes()); k } /// collision-proof report key: keccak(pylon || window || probe) #[inline(always)] fn probe_report_key(pylon: &[u8; 20], window: u32, probe: &[u8; 20]) -> [u8; 32] { let mut buf = [0u8; 44]; buf[0..20].copy_from_slice(pylon); buf[20..24].copy_from_slice(&window.to_le_bytes()); buf[24..44].copy_from_slice(probe); let mut h = [0u8; 32]; api::hash_keccak_256(&buf, &mut h); let mut key = [0u8; 32]; key[0] = PROBE_REPORT_PREFIX; key[1..32].copy_from_slice(&h[..31]); key } /// returns a pylon's status key #[inline(always)] fn pylon_status_key(pylon: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PYLON_STATUS_PREFIX; key[1..21].copy_from_slice(pylon); key } /// returns pylon's metrics key #[inline(always)] fn pylon_metrics_key(pylon: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PYLON_METRICS_PREFIX; key[1..21].copy_from_slice(pylon); key } /// returns probe's whitelist key #[inline(always)] fn probe_whitelist_key(probe: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PROBE_WHITELIST_PREFIX; key[1..21].copy_from_slice(probe); key } #[no_mangle] #[polkavm_derive::polkavm_export] pub extern "C" fn deploy() { // sets initial org labels and zeroes counters sset(&org_key(1), b"IBP"); sset(&org_key(2), b"dotters"); sset(&NETWORK_COUNT_KEY, &0u32.to_le_bytes()); sset(&PROPOSAL_COUNT_KEY, &0u32.to_le_bytes()); } #[no_mangle] #[polkavm_derive::polkavm_export] pub extern "C" fn call() { let mut selector = [0u8; 4]; api::call_data_copy(&mut selector, 0); // compute all selectors we support (runtime-keccak) let s_create_network = sel("createNetwork()"); let s_set_network_dns = sel("setNetworkDns(uint32,uint8,bool)"); let s_add_pylon = sel("addPylon(uint32,address)"); let s_remove_pylon = sel("removePylon(uint32,address)"); let s_set_pylon_org = sel("setPylonOrg(address,uint8)"); let s_set_pylon_level = sel("setPylonLevel(address,uint8)"); let s_set_dns_controller = sel("setDnsController(uint8,address)"); let s_get_dns_controller = sel("getDnsController(uint8)"); let s_whitelist_probe = sel("whitelistProbe(address)"); let s_revoke_probe = sel("revokeProbe(address)"); let s_is_probe_whitelisted = sel("isProbeWhitelisted(address)"); let s_propose = sel("propose(uint8)"); let s_vote = sel("vote(uint32,bool)"); let s_execute_proposal = sel("executeProposal(uint32)"); let s_get_network_info = sel("getNetworkInfo(uint32)"); let s_get_network_count = sel("getNetworkCount()"); let s_get_proposal = sel("getProposal(uint32)"); let s_get_pylon_level = sel("getPylonLevel(address)"); let s_get_pylon_status = sel("getPylonStatus(address)"); let s_get_pylon_metrics = sel("getPylonMetrics(address)"); let s_report_probe_data = sel("reportProbeData(address,bytes32,uint8)"); let s_finalize_window = sel("finalizeWindow(address,uint32)"); let s_bootstrap = sel("bootstrap()"); match selector { x if x == s_bootstrap => bootstrap(), x if x == s_create_network => create_network(), x if x == s_set_network_dns => set_network_dns(), x if x == s_add_pylon => add_pylon(), x if x == s_remove_pylon => remove_pylon(), x if x == s_set_pylon_org => set_pylon_org(), x if x == s_set_pylon_level => set_pylon_level(), x if x == s_set_dns_controller => set_dns_controller(), x if x == s_get_dns_controller => get_dns_controller(), x if x == s_whitelist_probe => whitelist_probe(), x if x == s_revoke_probe => revoke_probe(), x if x == s_is_probe_whitelisted => is_probe_whitelisted(), x if x == s_propose => propose(), x if x == s_vote => vote(), x if x == s_execute_proposal => execute_proposal(), x if x == s_get_network_info => get_network_info(), x if x == s_get_network_count => get_network_count(), x if x == s_get_proposal => get_proposal(), x if x == s_get_pylon_level => get_pylon_level(), x if x == s_get_pylon_status => get_pylon_status(), x if x == s_get_pylon_metrics => get_pylon_metrics(), x if x == s_report_probe_data => report_probe_data(), x if x == s_finalize_window => finalize_window(), _ => api::return_value(ReturnFlags::REVERT, &[]), } } /// one-time bootstrap to set deployer as templar fn bootstrap() { let mut caller = [0u8; 20]; api::caller(&mut caller); let mut level = [0u8; 1]; sget(&pylon_level_key(&caller), &mut level); if level[0] != 0 { api::return_value(ReturnFlags::REVERT, b"already bootstrapped"); } sset(&pylon_level_key(&caller), &[5u8]); api::return_value(ReturnFlags::empty(), &[]); } /// creates a new network; caller must be level 5+; caller becomes initial pylon of that network /// /// returns: `uint32 network_id` fn create_network() { let mut caller = [0u8; 20]; api::caller(&mut caller); let mut level = [0u8; 1]; sget(&pylon_level_key(&caller), &mut level); if level[0] < 5 { api::return_value(ReturnFlags::REVERT, b"need level 5+"); } let mut count_bytes = [0u8; 4]; sget(&NETWORK_COUNT_KEY, &mut count_bytes); let mut count = u32::from_le_bytes(count_bytes); count = count.saturating_add(1); let network_id = count; sset(&NETWORK_COUNT_KEY, &count.to_le_bytes()); sset(&network_key(network_id), &level); sset(&pylon_key(network_id, &caller), &[1u8]); // mark as member emit_network_created(network_id, &caller, level[0]); let mut out = [0u8; 32]; out[28..32].copy_from_slice(&network_id.to_be_bytes()); api::return_value(ReturnFlags::empty(), &out); } /// sets dns enable flag for an org on a given network; controller or level 5+ pylon is required fn set_network_dns() { let mut input = [0u8; 96]; // selector-skipped args (3 slots) api::call_data_copy(&mut input, 4); let network_id = u32::from_be_bytes([input[28], input[29], input[30], input[31]]); let org_id = input[63]; let enabled = input[95] != 0; if !(org_id == 1 || org_id == 2) { api::return_value(ReturnFlags::REVERT, b"invalid org"); } // ensure network exists let mut level_req = [0u8; 1]; sget(&network_key(network_id), &mut level_req); if level_req[0] == 0 { api::return_value(ReturnFlags::REVERT, b"network not found"); } let mut caller = [0u8; 20]; api::caller(&mut caller); let mut controller = [0u8; 20]; sget(&dns_controller_key(org_id), &mut controller); let mut is_pylon = [0u8; 1]; sget(&pylon_key(network_id, &caller), &mut is_pylon); let mut level = [0u8; 1]; sget(&pylon_level_key(&caller), &mut level); if caller != controller && (is_pylon[0] == 0 || level[0] < 5) { api::return_value(ReturnFlags::REVERT, b"not authorized"); } sset(&dns_key(network_id, org_id), &[enabled as u8]); api::return_value(ReturnFlags::empty(), &[]); } /// adds a pylon to a network; caller must be a level 5+ pylon of that network fn add_pylon() { let mut input = [0u8; 64]; // 2 slots api::call_data_copy(&mut input, 4); let network_id = u32::from_be_bytes([input[28], input[29], input[30], input[31]]); let mut new_pylon = [0u8; 20]; new_pylon.copy_from_slice(&input[44..64]); // ensure network exists let mut net_data = [0u8; 1]; sget(&network_key(network_id), &mut net_data); if net_data[0] == 0 { api::return_value(ReturnFlags::REVERT, b"network not found"); } let mut caller = [0u8; 20]; api::caller(&mut caller); let mut is_pylon = [0u8; 1]; sget(&pylon_key(network_id, &caller), &mut is_pylon); let mut level = [0u8; 1]; sget(&pylon_level_key(&caller), &mut level); if is_pylon[0] == 0 || level[0] < 5 { api::return_value(ReturnFlags::REVERT, b"not authorized"); } sset(&pylon_key(network_id, &new_pylon), &[1u8]); sset(&pylon_status_key(&new_pylon), &[0u8]); // default as healthy api::return_value(ReturnFlags::empty(), &[]); } /// removing a pylon must be done via governance fn remove_pylon() { api::return_value(ReturnFlags::REVERT, b"use governance"); } /// creates a proposal to set a pylon's org; only level 5+ may propose /// /// returns: `uint32 proposal_id` fn set_pylon_org() { let mut input = [0u8; 64]; api::call_data_copy(&mut input, 4); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&input[12..32]); let org_id = input[63]; if !(org_id == 1 || org_id == 2) { api::return_value(ReturnFlags::REVERT, b"invalid org"); } create_proposal(2, &pylon, org_id); } /// creates a proposal to set a pylon's level; only level 5+ may propose /// /// returns: `uint32 proposal_id` fn set_pylon_level() { let mut input = [0u8; 64]; api::call_data_copy(&mut input, 4); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&input[12..32]); let new_level = input[63]; if new_level > 9 { api::return_value(ReturnFlags::REVERT, b"max level 9"); } create_proposal(1, &pylon, new_level); } /// creates a proposal to set dns controller for an org /// /// returns: `uint32 proposal_id` fn set_dns_controller() { let mut input = [0u8; 64]; api::call_data_copy(&mut input, 4); let org_id = input[31]; let mut controller = [0u8; 20]; controller.copy_from_slice(&input[44..64]); if !(org_id == 1 || org_id == 2) { api::return_value(ReturnFlags::REVERT, b"invalid org"); } create_proposal(3, &controller, org_id); } /// creates a proposal to whitelist a probe /// /// returns: `uint32 proposal_id` fn whitelist_probe() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let mut probe = [0u8; 20]; probe.copy_from_slice(&input[12..32]); create_proposal(4, &probe, 1); } /// creates a proposal to revoke a probe /// /// returns: `uint32 proposal_id` fn revoke_probe() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let mut probe = [0u8; 20]; probe.copy_from_slice(&input[12..32]); create_proposal(5, &probe, 0); } /// creates a governance proposal (internal helper) fn create_proposal(proposal_type: u8, target: &[u8; 20], value: u8) { let mut caller = [0u8; 20]; api::caller(&mut caller); let mut level = [0u8; 1]; sget(&pylon_level_key(&caller), &mut level); if level[0] < 5 { api::return_value(ReturnFlags::REVERT, b"need level 5+"); } let mut count_bytes = [0u8; 4]; sget(&PROPOSAL_COUNT_KEY, &mut count_bytes); let mut count = u32::from_le_bytes(count_bytes); count = count.saturating_add(1); // meta: [0]=type, [1..21]=proposer let mut meta = [0u8; 32]; meta[0] = proposal_type; meta[1..21].copy_from_slice(&caller); sset(&proposal_key(count), &meta); // data: [0..20]=target, [20]=value let mut data = [0u8; 32]; data[0..20].copy_from_slice(target); data[20] = value; sset(&proposal_data_key(count), &data); sset(&PROPOSAL_COUNT_KEY, &count.to_le_bytes()); emit_proposal_created(count, &caller, proposal_type); let mut out = [0u8; 32]; out[28..32].copy_from_slice(&count.to_be_bytes()); api::return_value(ReturnFlags::empty(), &out); } /// generic propose entrypoint is disabled to avoid abi misuse fn propose() { api::return_value(ReturnFlags::REVERT, b"use typed propose"); } /// casts a vote on a proposal; equal weight for levels 5..7 /// /// returns: `uint16 tally_for_side` fn vote() { let mut input = [0u8; 64]; api::call_data_copy(&mut input, 4); let proposal_id = u32::from_be_bytes([input[28], input[29], input[30], input[31]]); let support = input[63] != 0; let mut voter = [0u8; 20]; api::caller(&mut voter); let mut level = [0u8; 1]; sget(&pylon_level_key(&voter), &mut level); if level[0] < 5 { api::return_value(ReturnFlags::REVERT, b"need level 5+"); } let mut existing = [0u8; 1]; sget(&vote_key(proposal_id, &voter), &mut existing); if existing[0] != 0 { api::return_value(ReturnFlags::REVERT, b"already voted"); } let weight = calculate_vote_weight(level[0]); sset(&vote_key(proposal_id, &voter), &[support as u8 + 1]); let mut tall = [0u8; 2]; sget(&vote_weight_key(proposal_id, support), &mut tall); let mut tally = u16::from_le_bytes(tall); tally = tally.saturating_add(weight as u16); sset(&vote_weight_key(proposal_id, support), &tally.to_le_bytes()); emit_proposal_voted(proposal_id, &voter, support, weight); let mut out = [0u8; 32]; out[30..32].copy_from_slice(&tally.to_le_bytes()); api::return_value(ReturnFlags::empty(), &out); } /// executes a proposal if a 2/3 majority is met fn execute_proposal() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let proposal_id = u32::from_be_bytes([input[28], input[29], input[30], input[31]]); // prevent re-execution let mut ex = [0u8; 1]; sget(&proposal_executed_key(proposal_id), &mut ex); if ex[0] != 0 { api::return_value(ReturnFlags::REVERT, b"already executed"); } let mut yes_bytes = [0u8; 2]; sget(&vote_weight_key(proposal_id, true), &mut yes_bytes); let yes = u16::from_le_bytes(yes_bytes); let mut no_bytes = [0u8; 2]; sget(&vote_weight_key(proposal_id, false), &mut no_bytes); let no = u16::from_le_bytes(no_bytes); let total = yes + no; // use ceiling division: (total * 2 + 2) / 3 let threshold = (total * 2 + 2) / 3; if total == 0 || yes < threshold { api::return_value(ReturnFlags::REVERT, b"need 2/3 majority"); } let mut meta = [0u8; 32]; sget(&proposal_key(proposal_id), &mut meta); let ptype = meta[0]; match ptype { 1 => apply_set_pylon_level(proposal_id), 2 => apply_set_pylon_org(proposal_id), 3 => apply_set_dns_controller(proposal_id), 4 => apply_whitelist_probe(proposal_id), 5 => apply_revoke_probe(proposal_id), _ => api::return_value(ReturnFlags::REVERT, b"invalid type"), } // mark executed let mut caller = [0u8; 20]; api::caller(&mut caller); emit_proposal_executed(proposal_id, &caller); sset(&proposal_executed_key(proposal_id), &[1u8]); // return proposal ID for monitoring let mut out = [0u8; 32]; out[28..32].copy_from_slice(&proposal_id.to_be_bytes()); api::return_value(ReturnFlags::empty(), &out); } /// applies a pylon level change decided by governance fn apply_set_pylon_level(proposal_id: u32) { let mut data = [0u8; 32]; sget(&proposal_data_key(proposal_id), &mut data); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&data[0..20]); let new_level = data[20]; sset(&pylon_level_key(&pylon), &[new_level]); } /// applies a pylon org change decided by governance fn apply_set_pylon_org(proposal_id: u32) { let mut data = [0u8; 32]; sget(&proposal_data_key(proposal_id), &mut data); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&data[0..20]); let org_id = data[20]; sset(&pylon_org_key(&pylon), &[org_id]); } /// applies a dns controller change decided by governance fn apply_set_dns_controller(proposal_id: u32) { let mut data = [0u8; 32]; sget(&proposal_data_key(proposal_id), &mut data); let mut controller = [0u8; 20]; controller.copy_from_slice(&data[0..20]); let org_id = data[20]; sset(&dns_controller_key(org_id), &controller); } /// whitelists a probe decided by governance fn apply_whitelist_probe(proposal_id: u32) { let mut data = [0u8; 32]; sget(&proposal_data_key(proposal_id), &mut data); let mut probe = [0u8; 20]; probe.copy_from_slice(&data[0..20]); sset(&probe_whitelist_key(&probe), &[1u8]); } /// revokes a probe decided by governance fn apply_revoke_probe(proposal_id: u32) { let mut data = [0u8; 32]; sget(&proposal_data_key(proposal_id), &mut data); let mut probe = [0u8; 20]; probe.copy_from_slice(&data[0..20]); sset(&probe_whitelist_key(&probe), &[0u8]); } /// returns network info: level requirement and org dns flags /// /// returns: `(uint8 level_requirement, bool ibp_dns_enabled, bool dotters_dns_enabled)` fn get_network_info() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let network_id = u32::from_be_bytes([input[28], input[29], input[30], input[31]]); let mut level_req = [0u8; 1]; sget(&network_key(network_id), &mut level_req); let mut ibp_dns = [0u8; 1]; sget(&dns_key(network_id, 1), &mut ibp_dns); let mut dotters_dns = [0u8; 1]; sget(&dns_key(network_id, 2), &mut dotters_dns); // abi: 3 slots (96 bytes) let mut out = [0u8; 96]; out[31] = level_req[0]; out[63] = ibp_dns[0]; out[95] = dotters_dns[0]; api::return_value(ReturnFlags::empty(), &out); } /// returns the total number of networks /// /// returns: `uint32 count` fn get_network_count() { let mut count_bytes = [0u8; 4]; sget(&NETWORK_COUNT_KEY, &mut count_bytes); let count = u32::from_le_bytes(count_bytes); let mut out = [0u8; 32]; out[28..32].copy_from_slice(&count.to_be_bytes()); api::return_value(ReturnFlags::empty(), &out); } /// returns dns controller for an org /// /// returns: `address controller` fn get_dns_controller() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let org_id = input[31]; if !(org_id == 1 || org_id == 2) { api::return_value(ReturnFlags::REVERT, b"invalid org"); } let mut controller = [0u8; 20]; sget(&dns_controller_key(org_id), &mut controller); let mut out = [0u8; 32]; out[12..32].copy_from_slice(&controller); api::return_value(ReturnFlags::empty(), &out); } /// returns proposal meta info /// /// returns: `(uint8 proposal_type, address proposer)` fn get_proposal() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let proposal_id = u32::from_be_bytes([input[28], input[29], input[30], input[31]]); let mut meta = [0u8; 32]; sget(&proposal_key(proposal_id), &mut meta); let mut out = [0u8; 64]; out[31] = meta[0]; out[44..64].copy_from_slice(&meta[1..21]); api::return_value(ReturnFlags::empty(), &out); } /// returns a pylon's level /// /// returns: `uint8 level` fn get_pylon_level() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&input[12..32]); let mut level = [0u8; 1]; sget(&pylon_level_key(&pylon), &mut level); let mut out = [0u8; 32]; out[31] = level[0]; api::return_value(ReturnFlags::empty(), &out); } /// returns a pylon's current status /// /// status: 0 healthy, 1 degraded, 2 insufficient reports, 128+ errors /// /// returns: `uint8 status` fn get_pylon_status() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&input[12..32]); let mut status = [0u8; 1]; sget(&pylon_status_key(&pylon), &mut status); let mut out = [0u8; 32]; out[31] = status[0]; api::return_value(ReturnFlags::empty(), &out); } /// returns aggregated pylon metrics for the last finalized window /// /// returns: `(uint8 status, uint16 avg_latency, uint8 total_count, uint8 healthy_count, uint32 window)` fn get_pylon_metrics() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&input[12..32]); let mut m = [0u8; 10]; sget(&pylon_metrics_key(&pylon), &mut m); let mut out = [0u8; 160]; // 5 slots out[31] = m[0]; // status out[62..64].copy_from_slice(&m[1..3]); // avg_latency out[95] = m[3]; // total_count out[127] = m[4]; // healthy_count out[156..160].copy_from_slice(&m[6..10]); // window api::return_value(ReturnFlags::empty(), &out); } /// checks whether a probe is whitelisted /// /// returns: `bool whitelisted` fn is_probe_whitelisted() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let mut probe = [0u8; 20]; probe.copy_from_slice(&input[12..32]); let mut wl = [0u8; 1]; sget(&probe_whitelist_key(&probe), &mut wl); let mut out = [0u8; 32]; out[31] = wl[0]; api::return_value(ReturnFlags::empty(), &out); } /// records probe report: hash (32 bytes) + status (1 byte) /// status codes: 0-127 healthy, 128-255 errors /// /// args: (address pylon, bytes32 reportHash, uint8 statusCode) /// returns: `uint32 window` fn report_probe_data() { let mut input = [0u8; 96]; // 3 slots api::call_data_copy(&mut input, 4); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&input[12..32]); let mut report_hash = [0u8; 32]; report_hash.copy_from_slice(&input[36..68]); // slot 1 let status_code = input[95]; // slot 2 // reject 255 sentinel value if status_code == 255 { api::return_value(ReturnFlags::REVERT, b"invalid status 255"); } let mut caller = [0u8; 20]; api::caller(&mut caller); let mut wl = [0u8; 1]; sget(&probe_whitelist_key(&caller), &mut wl); if wl[0] == 0 { api::return_value(ReturnFlags::REVERT, b"probe not whitelisted"); } let mut pylon_level = [0u8; 1]; sget(&pylon_level_key(&pylon), &mut pylon_level); if pylon_level[0] == 0 { api::return_value(ReturnFlags::REVERT, b"invalid pylon"); } let mut now32 = [0u8; 32]; api::now(&mut now32); let mut lo = [0u8; 8]; lo.copy_from_slice(&now32[..8]); let now = u64::from_le_bytes(lo); let window = (now / REPORT_INTERVAL) as u32; // enforce one report per probe per window let mut reported = [0u8; 1]; sget(&probe_report_key(&pylon, window, &caller), &mut reported); if reported[0] != 0 { api::return_value(ReturnFlags::REVERT, b"already reported"); } // store hash and status (33 bytes total) let mut report_data = [0u8; 33]; report_data[0..32].copy_from_slice(&report_hash); report_data[32] = status_code; sset(&probe_report_key(&pylon, window, &caller), &report_data); // simplified window data: [count, healthy_count, degraded_count, error_count] let mut wdata = [0u8; 4]; sget(&probe_window_key(&pylon, window), &mut wdata); wdata[0] = wdata[0].saturating_add(1); // total count if status_code < 128 { wdata[1] = wdata[1].saturating_add(1); // healthy count } else if status_code < 200 { wdata[2] = wdata[2].saturating_add(1); // degraded count } else { wdata[3] = wdata[3].saturating_add(1); // error count } sset(&probe_window_key(&pylon, window), &wdata); let mut out = [0u8; 32]; out[28..32].copy_from_slice(&window.to_be_bytes()); api::return_value(ReturnFlags::empty(), &out); } /// finalizes window with 2/3 consensus requirement /// /// returns: `uint8 status` fn finalize_window() { let mut input = [0u8; 64]; // 2 slots api::call_data_copy(&mut input, 4); let mut pylon = [0u8; 20]; pylon.copy_from_slice(&input[12..32]); let window = u32::from_be_bytes([input[60], input[61], input[62], input[63]]); let mut now32 = [0u8; 32]; api::now(&mut now32); let mut lo = [0u8; 8]; lo.copy_from_slice(&now32[..8]); let now = u64::from_le_bytes(lo); let current_window = (now / REPORT_INTERVAL) as u32; if window >= current_window { api::return_value(ReturnFlags::REVERT, b"window not complete"); } // prevent duplicate finalization let mut fin = [0u8; 1]; sget(&window_finalized_key(&pylon, window), &mut fin); if fin[0] != 0 { api::return_value(ReturnFlags::REVERT, b"already finalized"); } let mut wdata = [0u8; 4]; sget(&probe_window_key(&pylon, window), &mut wdata); let total_count = wdata[0]; let healthy_count = wdata[1]; let degraded_count = wdata[2]; let _error_count = wdata[3]; // mark as insufficient data if not enough probes if total_count < MIN_PROBES_FOR_CONSENSUS { sset(&pylon_status_key(&pylon), &[2u8]); // insufficient data sset(&window_finalized_key(&pylon, window), &[1u8]); emit_window_finalized(&pylon, window, 2, total_count); let mut out = [0u8; 32]; out[31] = 2; api::return_value(ReturnFlags::empty(), &out); } // 2/3 consensus for status determination #[allow(clippy::manual_div_ceil)] let consensus_threshold = (total_count * 2 + 2) / 3; let status = if healthy_count >= consensus_threshold { 0u8 // healthy } else if (healthy_count + degraded_count) >= consensus_threshold { 1u8 // degraded (mixed results but mostly reachable) } else { 128u8 // offline/error (majority reporting errors) }; sset(&pylon_status_key(&pylon), &[status]); // store simple metrics: status, counts, window let mut metrics = [0u8; 8]; metrics[0] = status; metrics[1] = total_count; metrics[2] = healthy_count; metrics[3] = degraded_count; metrics[4..8].copy_from_slice(&window.to_le_bytes()); sset(&pylon_metrics_key(&pylon), &metrics); // store metrics sset(&probe_window_key(&pylon, window), &[0u8; 4]); // clear accumulator sset(&window_finalized_key(&pylon, window), &[1u8]); // mark finalized emit_window_finalized(&pylon, window, status, total_count); let mut out = [0u8; 32]; out[31] = status; api::return_value(ReturnFlags::empty(), &out); } /// vote weight calculation for governance (equal weights across ranks 5..9) fn calculate_vote_weight(level: u8) -> u8 { match level { 5..=9 => 1, _ => 0, } } === src/main.rs === #![feature(alloc_error_handler)] #![no_main] #![no_std] #![allow(static_mut_refs)] use uapi::{CallFlags, HostFn, HostFnImpl as api, ReturnFlags, StorageFlags}; #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { unsafe { core::arch::asm!("unimp"); core::hint::unreachable_unchecked(); } } // minimal bump allocator for `alloc` mod alloc_support { use core::{ alloc::{GlobalAlloc, Layout}, sync::atomic::{AtomicUsize, Ordering}, }; pub struct BumpAllocator { offset: AtomicUsize, } const HEAP_SIZE: usize = 64 * 1024; #[link_section = ".bss.heap"] static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE]; unsafe impl GlobalAlloc for BumpAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { let align = layout.align().max(8); let size = layout.size(); let mut offset = self.offset.load(Ordering::Relaxed); loop { let aligned = (offset + align - 1) & !(align - 1); if aligned + size > HEAP_SIZE { return core::ptr::null_mut(); } match self.offset.compare_exchange_weak( offset, aligned + size, Ordering::SeqCst, Ordering::Relaxed, ) { Ok(_) => { let heap_ptr = HEAP.as_ptr() as *mut u8; return heap_ptr.add(aligned); }, Err(o) => offset = o, } } } unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} } #[global_allocator] static GLOBAL: BumpAllocator = BumpAllocator { offset: AtomicUsize::new(0) }; #[alloc_error_handler] fn alloc_error(_layout: Layout) -> ! { unsafe { core::arch::asm!("unimp"); core::hint::unreachable_unchecked(); } } } // tiny adapters for storage apis #[inline(always)] fn sset(key: &[u8], value: &[u8]) { let _ = api::set_storage(StorageFlags::empty(), key, value); } #[inline(always)] fn sget(key: &[u8], out: &mut [u8]) { let mut slice = &mut out[..]; let _ = api::get_storage(StorageFlags::empty(), key, &mut slice); } // shared with implementation (controller must read levels) const PYLON_LEVEL_PREFIX: u8 = 0x10; /// returns the storage key for a pylon's level (shared between controller and implementation) fn pylon_level_key(pylon: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = PYLON_LEVEL_PREFIX; key[1..21].copy_from_slice(pylon); key } // controller-only keys: use distinct high-value markers to avoid collision const IMPLEMENTATION_KEY: [u8; 32] = [0xA1; 32]; const PENDING_IMPL_KEY: [u8; 32] = [0xA2; 32]; const UPGRADE_PROPOSAL_KEY: [u8; 32] = [0xA3; 32]; const UPGRADE_YES_WEIGHT_KEY: [u8; 32] = [0xA4; 32]; const UPGRADE_NO_WEIGHT_KEY: [u8; 32] = [0xA5; 32]; const TEMPLAR_KEY: [u8; 32] = [0xA7; 32]; const UPGRADE_VOTE_PREFIX: u8 = 0xA6; const UPGRADE_ID_KEY: [u8; 32] = [0xA8; 32]; /// generates per-voter key for controller upgrade votes (versioned by proposal ID) fn upgrade_vote_key(upgrade_id: u32, voter: &[u8; 20]) -> [u8; 32] { let mut key = [0u8; 32]; key[0] = UPGRADE_VOTE_PREFIX; key[1..5].copy_from_slice(&upgrade_id.to_le_bytes()); key[5..25].copy_from_slice(voter); key } /// computes the first 4 bytes of keccak(signature) at runtime #[inline(always)] fn sel(signature: &str) -> [u8; 4] { let mut h = [0u8; 32]; api::hash_keccak_256(signature.as_bytes(), &mut h); [h[0], h[1], h[2], h[3]] } // event emission helpers /// emits UpgradeProposed(address indexed proposer, address indexed newImplementation, uint256 timestamp) fn emit_upgrade_proposed(proposer: &[u8; 20], new_impl: &[u8; 20]) { let mut topic0 = [0u8; 32]; // event signature - FULL hash api::hash_keccak_256(b"UpgradeProposed(address,address,uint256)", &mut topic0); let mut topic1 = [0u8; 32]; // proposer (indexed) topic1[12..32].copy_from_slice(proposer); let mut topic2 = [0u8; 32]; // newImplementation (indexed) topic2[12..32].copy_from_slice(new_impl); let mut data = [0u8; 32]; // timestamp api::now(&mut data); let topics = [topic0, topic1, topic2]; api::deposit_event(&topics, &data); } /// emits UpgradeVoted(address indexed voter, bool support, uint16 weight) fn emit_upgrade_voted(voter: &[u8; 20], support: bool, weight: u16) { let mut topic0 = [0u8; 32]; // event signature - FULL hash api::hash_keccak_256(b"UpgradeVoted(address,bool,uint16)", &mut topic0); let mut topic1 = [0u8; 32]; // voter (indexed) topic1[12..32].copy_from_slice(voter); let mut data = [0u8; 64]; data[31] = support as u8; data[62..64].copy_from_slice(&weight.to_be_bytes()); let topics = [topic0, topic1]; api::deposit_event(&topics, &data); } /// emits UpgradeExecuted(address indexed oldImplementation, address indexed newImplementation) fn emit_upgrade_executed(old_impl: &[u8; 20], new_impl: &[u8; 20]) { let mut topic0 = [0u8; 32]; // event signature - FULL hash api::hash_keccak_256(b"UpgradeExecuted(address,address)", &mut topic0); let mut topic1 = [0u8; 32]; // oldImplementation (indexed) topic1[12..32].copy_from_slice(old_impl); let mut topic2 = [0u8; 32]; // newImplementation (indexed) topic2[12..32].copy_from_slice(new_impl); let topics = [topic0, topic1, topic2]; api::deposit_event(&topics, &[]); } /// emits TemplarRemoved(address indexed templar) fn emit_templar_removed(templar: &[u8; 20]) { let mut topic0 = [0u8; 32]; // event signature - FULL hash api::hash_keccak_256(b"TemplarRemoved(address)", &mut topic0); let mut topic1 = [0u8; 32]; topic1[12..32].copy_from_slice(templar); let topics = [topic0, topic1]; api::deposit_event(&topics, &[]); } /// initializes proxy with implementation address and deployer as templar #[no_mangle] #[polkavm_derive::polkavm_export] pub extern "C" fn deploy() { let n = api::call_data_size() as usize; if n < 20 { // cannot deploy without implementation api::return_value(ReturnFlags::REVERT, b"missing implementation"); } let mut implementation = [0u8; 20]; api::call_data_copy(&mut implementation, (n - 20) as u32); // reject zero address if implementation == [0u8; 20] { api::return_value(ReturnFlags::REVERT, b"zero implementation"); } // check implementation has code let size = api::code_size(&implementation); if size == 0 { api::return_value(ReturnFlags::REVERT, b"no code at implementation"); } // set implementation sset(&IMPLEMENTATION_KEY, &implementation); // set deployer as templar let mut deployer = [0u8; 20]; api::caller(&mut deployer); sset(&TEMPLAR_KEY, &deployer); } /// routes calls to proxy functions or delegates to implementation #[no_mangle] #[polkavm_derive::polkavm_export] pub extern "C" fn call() { let mut selector = [0u8; 4]; api::call_data_copy(&mut selector, 0); // compute proxy selectors on the fly to avoid accidental collisions let s_propose_upgrade = sel("proposeUpgrade(address)"); let s_vote_upgrade = sel("voteUpgrade(bool)"); let s_execute_upgrade = sel("executeUpgrade()"); let s_get_implementation = sel("getImplementation()"); let s_get_pending_upgrade = sel("getPendingUpgrade()"); let s_remove_templar = sel("removeTemplar()"); match selector { x if x == s_propose_upgrade => propose_upgrade(), x if x == s_vote_upgrade => vote_upgrade(), x if x == s_execute_upgrade => execute_upgrade(), x if x == s_get_implementation => get_implementation(), x if x == s_get_pending_upgrade => get_pending_upgrade(), x if x == s_remove_templar => remove_templar(), _ => delegate_to_implementation(), } } /// proposes a new implementation for upgrade and clears previous tallies fn propose_upgrade() { let mut new_impl = [0u8; 20]; api::call_data_copy(&mut new_impl, 16); // reject zero address if new_impl == [0u8; 20] { api::return_value(ReturnFlags::REVERT, b"zero implementation"); } // get current implementation let mut current_impl = [0u8; 20]; sget(&IMPLEMENTATION_KEY, &mut current_impl); // reject if same as current if new_impl == current_impl { api::return_value(ReturnFlags::REVERT, b"same as current"); } let mut caller = [0u8; 20]; api::caller(&mut caller); // increment upgrade ID for new proposal let mut upgrade_id_bytes = [0u8; 4]; sget(&UPGRADE_ID_KEY, &mut upgrade_id_bytes); let mut upgrade_id = u32::from_le_bytes(upgrade_id_bytes); upgrade_id = upgrade_id.saturating_add(1); sset(&UPGRADE_ID_KEY, &upgrade_id.to_le_bytes()); // reset tallies and store pending impl sset(&UPGRADE_YES_WEIGHT_KEY, &[0u8; 2]); sset(&UPGRADE_NO_WEIGHT_KEY, &[0u8; 2]); sset(&PENDING_IMPL_KEY, &new_impl); // record proposal time (little-endian u64 in first 8 bytes) let mut timestamp = [0u8; 32]; api::now(&mut timestamp); sset(&UPGRADE_PROPOSAL_KEY, ×tamp[..8]); emit_upgrade_proposed(&caller, &new_impl); api::return_value(ReturnFlags::empty(), &[]); } /// casts a weighted vote for or against the pending upgrade fn vote_upgrade() { let mut input = [0u8; 32]; api::call_data_copy(&mut input, 4); let support = input[31] != 0; let mut pending = [0u8; 20]; sget(&PENDING_IMPL_KEY, &mut pending); if pending == [0u8; 20] { api::return_value(ReturnFlags::REVERT, b"no pending upgrade"); } // get current upgrade ID let mut upgrade_id_bytes = [0u8; 4]; sget(&UPGRADE_ID_KEY, &mut upgrade_id_bytes); let upgrade_id = u32::from_le_bytes(upgrade_id_bytes); if upgrade_id == 0 { api::return_value(ReturnFlags::REVERT, b"no active proposal"); } let mut caller = [0u8; 20]; api::caller(&mut caller); // prevent duplicate vote (versioned by upgrade ID) let mut existing_vote = [0u8; 1]; sget(&upgrade_vote_key(upgrade_id, &caller), &mut existing_vote); if existing_vote[0] != 0 { api::return_value(ReturnFlags::REVERT, b"already voted"); } // equal weight across ranks 5..7 let weight = get_voter_weight(&caller); if weight == 0 { api::return_value(ReturnFlags::REVERT, b"not authorized"); } sset(&upgrade_vote_key(upgrade_id, &caller), &[support as u8 + 1]); // update tally let key = if support { UPGRADE_YES_WEIGHT_KEY } else { UPGRADE_NO_WEIGHT_KEY }; let mut tally_bytes = [0u8; 2]; sget(&key, &mut tally_bytes); let mut tally = u16::from_le_bytes(tally_bytes); tally = tally.saturating_add(weight as u16); sset(&key, &tally.to_le_bytes()); emit_upgrade_voted(&caller, support, tally); let mut out = [0u8; 32]; out[30..32].copy_from_slice(&tally.to_be_bytes()); api::return_value(ReturnFlags::empty(), &out); } /// executes the upgrade if a 2/3 majority is met and timelock has passed fn execute_upgrade() { let mut pending = [0u8; 20]; sget(&PENDING_IMPL_KEY, &mut pending); if pending == [0u8; 20] { api::return_value(ReturnFlags::REVERT, b"no pending upgrade"); } let mut yes_bytes = [0u8; 2]; sget(&UPGRADE_YES_WEIGHT_KEY, &mut yes_bytes); let yes = u16::from_le_bytes(yes_bytes); let mut no_bytes = [0u8; 2]; sget(&UPGRADE_NO_WEIGHT_KEY, &mut no_bytes); let no = u16::from_le_bytes(no_bytes); let total = yes + no; // use ceiling division: (total * 2 + 2) / 3 let threshold = (total * 2 + 2) / 3; if total == 0 || yes < threshold { api::return_value(ReturnFlags::REVERT, b"need 2/3 majority"); } // templar can bypass timelock once let mut templar = [0u8; 20]; sget(&TEMPLAR_KEY, &mut templar); let mut caller = [0u8; 20]; api::caller(&mut caller); let mut old_impl = [0u8; 20]; sget(&IMPLEMENTATION_KEY, &mut old_impl); if templar != [0u8; 20] && caller == templar { sset(&IMPLEMENTATION_KEY, &pending); sset(&PENDING_IMPL_KEY, &[0u8; 20]); sset(&UPGRADE_YES_WEIGHT_KEY, &[0u8; 2]); sset(&UPGRADE_NO_WEIGHT_KEY, &[0u8; 2]); emit_upgrade_executed(&old_impl, &pending); // get current upgrade ID to return let mut upgrade_id_bytes = [0u8; 4]; sget(&UPGRADE_ID_KEY, &mut upgrade_id_bytes); let upgrade_id = u32::from_le_bytes(upgrade_id_bytes); let mut out = [0u8; 32]; out[28..32].copy_from_slice(&upgrade_id.to_be_bytes()); api::return_value(ReturnFlags::empty(), &out); } let mut ts = [0u8; 8]; sget(&UPGRADE_PROPOSAL_KEY, &mut ts); let proposed = u64::from_le_bytes(ts); let mut now32 = [0u8; 32]; api::now(&mut now32); let mut lo = [0u8; 8]; lo.copy_from_slice(&now32[..8]); let now = u64::from_le_bytes(lo); // 48h delay (28800*6s blocks) if now < proposed.saturating_add(28_800) { api::return_value(ReturnFlags::REVERT, b"48h delay required"); } sset(&IMPLEMENTATION_KEY, &pending); sset(&PENDING_IMPL_KEY, &[0u8; 20]); sset(&UPGRADE_YES_WEIGHT_KEY, &[0u8; 2]); sset(&UPGRADE_NO_WEIGHT_KEY, &[0u8; 2]); emit_upgrade_executed(&old_impl, &pending); // get current upgrade ID to return let mut upgrade_id_bytes = [0u8; 4]; sget(&UPGRADE_ID_KEY, &mut upgrade_id_bytes); let upgrade_id = u32::from_le_bytes(upgrade_id_bytes); let mut out = [0u8; 32]; out[28..32].copy_from_slice(&upgrade_id.to_be_bytes()); api::return_value(ReturnFlags::empty(), &out); } /// permanently removes the templar privilege fn remove_templar() { let mut templar = [0u8; 20]; sget(&TEMPLAR_KEY, &mut templar); let mut caller = [0u8; 20]; api::caller(&mut caller); if caller != templar { api::return_value(ReturnFlags::REVERT, b"not templar"); } sset(&TEMPLAR_KEY, &[0u8; 20]); emit_templar_removed(&templar); api::return_value(ReturnFlags::empty(), &[]); } /// returns the current implementation address fn get_implementation() { let mut impl_addr = [0u8; 20]; sget(&IMPLEMENTATION_KEY, &mut impl_addr); let mut out = [0u8; 32]; out[12..32].copy_from_slice(&impl_addr); api::return_value(ReturnFlags::empty(), &out); } /// returns the pending implementation address (if any) fn get_pending_upgrade() { let mut pending = [0u8; 20]; sget(&PENDING_IMPL_KEY, &mut pending); let mut out = [0u8; 32]; out[12..32].copy_from_slice(&pending); api::return_value(ReturnFlags::empty(), &out); } /// returns the voting weight for an address based on IBP level (equal weight for 5..7) fn get_voter_weight(voter: &[u8; 20]) -> u8 { let mut level = [0u8; 1]; sget(&pylon_level_key(voter), &mut level); match level[0] { 5 | 6 | 7 => 1, _ => 0, } } /// delegates unrecognized calls to the implementation in the controller context fn delegate_to_implementation() { let mut implementation = [0u8; 20]; sget(&IMPLEMENTATION_KEY, &mut implementation); // check if implementation is set if implementation == [0u8; 20] { api::return_value(ReturnFlags::REVERT, b"no implementation"); } let data_len = api::call_data_size() as usize; const MAX_DATA: usize = 8192; if data_len > MAX_DATA { api::return_value(ReturnFlags::REVERT, b"data too large"); } let mut data_buf = [0u8; MAX_DATA]; let data = &mut data_buf[..data_len]; api::call_data_copy(data, 0); let mut out_buf = [0u8; MAX_DATA]; let mut out = &mut out_buf[..]; // guard against zero gas let gas_limit = api::gas_limit(); if gas_limit == 0 { api::return_value(ReturnFlags::REVERT, b"zero gas limit"); } match api::delegate_call( CallFlags::empty(), &implementation, gas_limit * 9 / 10, 0, &[0u8; 32], data, Some(&mut out), ) { Ok(()) => api::return_value(ReturnFlags::empty(), out), Err(_) => api::return_value(ReturnFlags::REVERT, &[]), } }