Compare commits

..

No commits in common. "4f68bc964b124829adb6a37a55b7f183edfdbffa" and "ef72a5650176b6695595f6c947ad1b96e32b5762" have entirely different histories.

11 changed files with 28 additions and 600 deletions

View file

@ -8,10 +8,10 @@ on:
jobs: jobs:
verify: verify:
runs-on: docker runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: https://github.com/actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: "25" java-version: "25"
@ -19,7 +19,7 @@ jobs:
publish: publish:
if: startsWith(gitea.ref, 'refs/tags/v') if: startsWith(gitea.ref, 'refs/tags/v')
needs: verify needs: verify
runs-on: docker runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4

View file

@ -58,7 +58,6 @@ enum class OpCode(val code: Byte) {
data class Instruction(val opCode: OpCode, val immediate: ByteArray = byteArrayOf()) { data class Instruction(val opCode: OpCode, val immediate: ByteArray = byteArrayOf()) {
init { init {
require(opCode != OpCode.PUSH32 || immediate.size == 32) require(opCode != OpCode.PUSH32 || immediate.size == 32)
require(opCode == OpCode.PUSH32 || immediate.isEmpty())
} }
} }
@ -79,10 +78,8 @@ object TwistBytecode {
while (input.hasRemaining()) { while (input.hasRemaining()) {
val code = input.get() val code = input.get()
val op = OpCode.entries.firstOrNull { it.code == code } ?: error("Unknown opcode") val op = OpCode.entries.firstOrNull { it.code == code } ?: error("Unknown opcode")
val immediate = if (op == OpCode.PUSH32) { val immediate =
require(input.remaining() >= 32) { "Truncated PUSH32 immediate" } if (op == OpCode.PUSH32) ByteArray(32).also(input::get) else byteArrayOf()
ByteArray(32).also(input::get)
} else byteArrayOf()
output += Instruction(op, immediate) output += Instruction(op, immediate)
} }
return output return output

View file

@ -43,49 +43,7 @@ data class ContractIr(
val functions: List<FunctionIr>, val functions: List<FunctionIr>,
val accessPolicy: IrAccessPolicy = IrAccessPolicy(), val accessPolicy: IrAccessPolicy = IrAccessPolicy(),
) { ) {
/** fun bytecode(): ByteArray = TwistBytecode.encode(functions.flatMap { it.instructions })
* Encodes a deployed contract. Calldata word zero is the function selector and all
* function arguments begin at word one. A non-matching selector deterministically reverts.
*/
fun bytecode(): ByteArray {
require(functions.map { it.selector }.distinct().size == functions.size) { "Duplicate function selector" }
val dispatchSize = 2 + functions.size * 5 + 1
var nextBody = dispatchSize
val targets = functions.associate { function ->
function.selector to nextBody.also { nextBody += function.instructions.size }
}
val output = mutableListOf(
Instruction(OpCode.PUSH32, Word256.ZERO.toBytes()),
Instruction(OpCode.CALLDATA_LOAD),
)
functions.forEach { function ->
output += Instruction(OpCode.DUP)
output += Instruction(OpCode.PUSH32, Word256.fromLong(function.selector.toLong()).toBytes())
output += Instruction(OpCode.EQ)
output += Instruction(OpCode.PUSH32, Word256.fromLong(targets.getValue(function.selector).toLong()).toBytes())
output += Instruction(OpCode.JUMPI)
}
output += Instruction(OpCode.REVERT)
functions.forEach { function ->
val offset = targets.getValue(function.selector)
output += function.instructions.mapIndexed { index, instruction ->
if (
instruction.opCode == OpCode.PUSH32 &&
function.instructions.getOrNull(index + 1)?.opCode in setOf(OpCode.JUMP, OpCode.JUMPI)
) {
Instruction(
OpCode.PUSH32,
Word256.of(
Word256.fromBytes(instruction.immediate).toBigInteger() +
java.math.BigInteger.valueOf(offset.toLong())
)
.toBytes(),
)
} else instruction
}
}
return TwistBytecode.encode(output)
}
} }
data class FunctionIr(val name: String, val selector: Int, val instructions: List<Instruction>) data class FunctionIr(val name: String, val selector: Int, val instructions: List<Instruction>)
@ -125,34 +83,6 @@ class FunctionBuilder {
fun callValue() = op(OpCode.CALLVALUE) fun callValue() = op(OpCode.CALLVALUE)
fun add() = op(OpCode.ADD)
fun subtract() = op(OpCode.SUB)
fun multiply() = op(OpCode.MUL)
fun divide() = op(OpCode.DIV)
fun equal() = op(OpCode.EQ)
fun lessThan() = op(OpCode.LT)
fun greaterThan() = op(OpCode.GT)
fun jump() = op(OpCode.JUMP)
fun jumpIf() = op(OpCode.JUMPI)
fun duplicate() = op(OpCode.DUP)
fun discard() = op(OpCode.POP)
fun calldataSize() = op(OpCode.CALLDATA_SIZE)
fun codeSize() = op(OpCode.CODE_SIZE)
fun gasRemaining() = op(OpCode.GAS)
fun emitLog() = op(OpCode.LOG) fun emitLog() = op(OpCode.LOG)
fun returnWord() = op(OpCode.RETURN) fun returnWord() = op(OpCode.RETURN)

View file

@ -1,285 +0,0 @@
package rip.crit.twist.compiler
import java.math.BigInteger
import rip.crit.twist.bytecode.Instruction
import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.jit.LLVMBuilder
import rip.crit.twist.tvm.Word256
/**
* A compact ML-inspired source language for contracts.
*
* Functions take 256-bit calldata words, `let` is lexical, and `load` and `store` are
* explicit state effects. It deliberately has no implicit conversions or host calls.
*/
class TwistMlCompiler {
fun parse(source: String): TwistMlContract = Parser(source).contract()
fun compile(source: String): ContractIr = compile(parse(source))
fun compile(contract: TwistMlContract): ContractIr =
ContractIr(
contract.name,
contract.functions.mapIndexed { index, function ->
FunctionIr(function.name, index + 1, Lowerer(function.parameters).lower(function.body))
},
)
/** Lowers pure arithmetic and explicit storage effects to portable LLVM IR. */
fun lowerToLlvm(source: String, moduleName: String = "twistml"): LLVMBuilder {
val contract = parse(source)
return LLVMBuilder(moduleName, LlvmLowerer(contract).lower())
}
}
private class LlvmLowerer(private val contract: TwistMlContract) {
private var value = 0
private var label = 0
private val lines = mutableListOf<String>()
private var needsLoad = false
private var needsStore = false
private var needsCaller = false
private var needsCallValue = false
fun lower(): String {
contract.functions.forEach { function -> lowerFunction(function) }
return buildString {
append("target triple = \"unknown-unknown-unknown\"\n")
if (needsLoad) append("declare i256 @twist_sload(i256)\n")
if (needsStore) append("declare void @twist_sstore(i256, i256)\n")
if (needsCaller) append("declare i256 @twist_caller()\n")
if (needsCallValue) append("declare i256 @twist_callvalue()\n")
append(lines.joinToString("\n"))
}
}
private fun lowerFunction(function: TwistMlFunction) {
value = 0
label = 0
val arguments = function.parameters.mapIndexed { index, _ -> "i256 %arg$index" }
lines += "define i256 @${function.name}(" + arguments.joinToString(", ") + ") {"
lines += "entry:"
val environment = function.parameters.mapIndexed { index, name -> name to "%arg$index" }.toMap()
val result = emit(function.body, environment)
lines += " ret i256 $result"
lines += "}"
}
private fun emit(expression: TwistMlExpr, environment: Map<String, String>): String = when (expression) {
is TwistMlExpr.Number -> expression.value.toString()
is TwistMlExpr.Variable -> environment[expression.name] ?: error("Unknown variable ${expression.name}")
is TwistMlExpr.Let -> emit(expression.body, environment + (expression.name to emit(expression.value, environment)))
TwistMlExpr.Caller -> {
needsCaller = true
call("i256 @twist_caller()")
}
TwistMlExpr.CallValue -> {
needsCallValue = true
call("i256 @twist_callvalue()")
}
is TwistMlExpr.Load -> {
needsLoad = true
call("i256 @twist_sload(${emit(expression.key, environment)})")
}
is TwistMlExpr.Store -> {
needsStore = true
lines += " call void @twist_sstore(i256 ${emit(expression.key, environment)}, i256 ${emit(expression.value, environment)})"
"0"
}
is TwistMlExpr.Binary -> {
val left = emit(expression.left, environment)
val right = emit(expression.right, environment)
when (expression.operator) {
"+" -> instruction("add i256 $left, $right")
"-" -> instruction("sub i256 $left, $right")
"*" -> instruction("mul i256 $left, $right")
"/" -> instruction("udiv i256 $left, $right")
"%" -> instruction("urem i256 $left, $right")
"&&" -> instruction("and i256 $left, $right")
"||" -> instruction("or i256 $left, $right")
"=", "==", "<", ">" -> {
val predicate = mapOf("=" to "eq", "==" to "eq", "<" to "ult", ">" to "ugt").getValue(expression.operator)
val comparison = instruction("icmp $predicate i256 $left, $right")
instruction("zext i1 $comparison to i256")
}
else -> error("Unsupported operator ${expression.operator}")
}
}
is TwistMlExpr.If -> {
val condition = emit(expression.condition, environment)
val test = instruction("icmp ne i256 $condition, 0")
val id = label++
val thenLabel = "then$id"
val elseLabel = "else$id"
val mergeLabel = "merge$id"
lines += " br i1 $test, label %$thenLabel, label %$elseLabel"
lines += "$thenLabel:"
val thenValue = emit(expression.thenBranch, environment)
lines += " br label %$mergeLabel"
lines += "$elseLabel:"
val elseValue = emit(expression.elseBranch, environment)
lines += " br label %$mergeLabel"
lines += "$mergeLabel:"
instruction("phi i256 [$thenValue, %$thenLabel], [$elseValue, %$elseLabel]")
}
}
private fun call(body: String): String = instruction("call $body")
private fun instruction(body: String): String = "%v${value++}".also { lines += " $it = $body" }
}
data class TwistMlContract(val name: String, val functions: List<TwistMlFunction>)
data class TwistMlFunction(val name: String, val parameters: List<String>, val body: TwistMlExpr)
sealed interface TwistMlExpr {
data class Number(val value: BigInteger) : TwistMlExpr
data class Variable(val name: String) : TwistMlExpr
data class Let(val name: String, val value: TwistMlExpr, val body: TwistMlExpr) : TwistMlExpr
data class If(val condition: TwistMlExpr, val thenBranch: TwistMlExpr, val elseBranch: TwistMlExpr) : TwistMlExpr
data class Binary(val operator: String, val left: TwistMlExpr, val right: TwistMlExpr) : TwistMlExpr
data class Load(val key: TwistMlExpr) : TwistMlExpr
data class Store(val key: TwistMlExpr, val value: TwistMlExpr) : TwistMlExpr
data object Caller : TwistMlExpr
data object CallValue : TwistMlExpr
}
private class Lowerer(parameters: List<String>) {
private val parameterValues = parameters.mapIndexed { index, name ->
name to listOf(Instruction(OpCode.PUSH32, Word256.fromLong(((index + 1) * 32).toLong()).toBytes()), Instruction(OpCode.CALLDATA_LOAD))
}.toMap()
fun lower(expression: TwistMlExpr): List<Instruction> = expression.lower(parameterValues) + Instruction(OpCode.RETURN)
private fun TwistMlExpr.lower(environment: Map<String, List<Instruction>>): List<Instruction> = when (this) {
is TwistMlExpr.Number -> listOf(Instruction(OpCode.PUSH32, Word256.of(value).toBytes()))
is TwistMlExpr.Variable -> environment[name]?.toList() ?: error("Unknown variable $name")
is TwistMlExpr.Let -> body.lower(environment + (name to value.lower(environment)))
is TwistMlExpr.Binary -> left.lower(environment) + right.lower(environment) + Instruction(operator.opcode())
is TwistMlExpr.Load -> key.lower(environment) + Instruction(OpCode.SLOAD)
is TwistMlExpr.Store -> key.lower(environment) + value.lower(environment) + Instruction(OpCode.SSTORE) + Instruction(OpCode.PUSH32, Word256.ZERO.toBytes())
TwistMlExpr.Caller -> listOf(Instruction(OpCode.CALLER))
TwistMlExpr.CallValue -> listOf(Instruction(OpCode.CALLVALUE))
is TwistMlExpr.If -> {
val conditionCode = condition.lower(environment)
val elseCode = elseBranch.lower(environment)
val thenCode = thenBranch.lower(environment)
val thenStart = conditionCode.size + elseCode.size + 4
val end = thenStart + thenCode.size
conditionCode +
Instruction(OpCode.PUSH32, Word256.fromLong(thenStart.toLong()).toBytes()) +
Instruction(OpCode.JUMPI) +
elseCode +
Instruction(OpCode.PUSH32, Word256.fromLong(end.toLong()).toBytes()) +
Instruction(OpCode.JUMP) +
thenCode
}
}
private fun String.opcode(): OpCode = when (this) {
"+" -> OpCode.ADD
"-" -> OpCode.SUB
"*" -> OpCode.MUL
"/" -> OpCode.DIV
"%" -> OpCode.MOD
"=", "==" -> OpCode.EQ
"<" -> OpCode.LT
">" -> OpCode.GT
"&&" -> OpCode.AND
"||" -> OpCode.OR
else -> error("Unsupported operator $this")
}
}
private class Parser(source: String) {
private val tokens = Regex("[A-Za-z_][A-Za-z0-9_]*|[0-9]+|&&|\\|\\||==|[{}(),;=+*/%<>-]")
.findAll(source).map { it.value }.toList()
private var position = 0
fun contract(): TwistMlContract {
expect("contract")
val name = identifier()
expect("{")
val functions = buildList {
while (peek() != "}") add(function())
}
expect("}")
require(position == tokens.size) { "Unexpected token ${peek()}" }
return TwistMlContract(name, functions)
}
private fun function(): TwistMlFunction {
expect("fun")
val name = identifier()
expect("(")
val parameters = mutableListOf<String>()
if (peek() != ")") do { parameters += identifier() } while (takeIf(","))
expect(")")
expect("=")
val body = expression()
takeIf(";")
return TwistMlFunction(name, parameters, body)
}
private fun expression(): TwistMlExpr {
if (takeIf("let")) {
val name = identifier()
expect("=")
val value = expression()
expect("in")
return TwistMlExpr.Let(name, value, expression())
}
if (takeIf("if")) {
val condition = expression()
expect("then")
val yes = expression()
expect("else")
return TwistMlExpr.If(condition, yes, expression())
}
return binary(0)
}
private fun binary(minimum: Int): TwistMlExpr {
var left = primary()
while (precedence(peek()) >= minimum) {
val operator = next()
val precedence = precedence(operator)
left = TwistMlExpr.Binary(operator, left, binary(precedence + 1))
}
return left
}
private fun primary(): TwistMlExpr = when (val token = next()) {
"(" -> expression().also { expect(")") }
"load" -> callOne(TwistMlExpr::Load)
"store" -> {
expect("(")
val key = expression()
expect(",")
TwistMlExpr.Store(key, expression()).also { expect(")") }
}
"caller" -> TwistMlExpr.Caller
"callvalue" -> TwistMlExpr.CallValue
else -> token.toBigIntegerOrNull()?.let(TwistMlExpr::Number) ?: TwistMlExpr.Variable(token)
}
private fun callOne(build: (TwistMlExpr) -> TwistMlExpr): TwistMlExpr {
expect("(")
return build(expression()).also { expect(")") }
}
private fun precedence(token: String?): Int = when (token) {
"||" -> 1
"&&" -> 2
"=", "==", "<", ">" -> 3
"+", "-" -> 4
"*", "/", "%" -> 5
else -> -1
}
private fun identifier(): String = next().also { require(it.matches(Regex("[A-Za-z_][A-Za-z0-9_]*"))) }
private fun peek(): String? = tokens.getOrNull(position)
private fun next(): String = peek()?.also { position++ } ?: error("Unexpected end of source")
private fun expect(value: String) = require(next() == value) { "Expected $value" }
private fun takeIf(value: String): Boolean = (peek() == value).also { if (it) position++ }
}

View file

@ -24,13 +24,5 @@ class PersistentToken(override val symbol: String, private val store: KeyValueSt
store.put(key(to), (balanceOf(to).value + amount.value).toByteArray()) store.put(key(to), (balanceOf(to).value + amount.value).toByteArray())
} }
fun burn(from: Address, amount: Amount): Boolean =
synchronized(this) {
val balance = balanceOf(from).value
if (amount.value.signum() <= 0 || balance < amount.value) return false
store.put(key(from), (balance - amount.value).toByteArray())
true
}
private fun key(owner: Address) = "$symbol:${owner.value}".encodeToByteArray() private fun key(owner: Address) = "$symbol:${owner.value}".encodeToByteArray()
} }

View file

@ -18,8 +18,6 @@ data class TwistoMetadata(
val name: String = "Twisto", val name: String = "Twisto",
val symbol: String = "TWISTO", val symbol: String = "TWISTO",
val description: String, val description: String,
val decimals: Int = 18,
val maxSupply: String? = null,
val image: String? = null, val image: String? = null,
val externalUrl: String? = null, val externalUrl: String? = null,
val issuerDid: String? = null, val issuerDid: String? = null,
@ -30,8 +28,6 @@ data class TwistoMetadata(
require(name.isNotBlank() && name.length <= 128) require(name.isNotBlank() && name.length <= 128)
require(symbol.matches(Regex("[A-Z0-9]{2,12}"))) require(symbol.matches(Regex("[A-Z0-9]{2,12}")))
require(description.length <= 4_096) require(description.length <= 4_096)
require(decimals in 0..36)
require(maxSupply == null || (maxSupply.toBigIntegerOrNull()?.signum() ?: -1) >= 0)
require( require(
attributes.size <= 64 && attributes.size <= 64 &&
attributes.all { it.key.length <= 64 && it.value.length <= 512 } attributes.all { it.key.length <= 64 && it.value.length <= 512 }
@ -48,10 +44,6 @@ data class TwistoMetadata(
append(symbol.escape()) append(symbol.escape())
append("\",\"description\":\"") append("\",\"description\":\"")
append(description.escape()) append(description.escape())
append("\",\"decimals\":")
append(decimals)
append(",\"maxSupply\":")
appendJson(maxSupply)
append("\",\"image\":") append("\",\"image\":")
appendJson(image) appendJson(image)
append(",\"externalUrl\":") append(",\"externalUrl\":")
@ -90,14 +82,9 @@ class TwistoToken(
synchronized(this) { synchronized(this) {
check(state.get(DEPLOYED) == null) { "Twisto is already deployed" } check(state.get(DEPLOYED) == null) { "Twisto is already deployed" }
require(metadata.symbol == SYMBOL) require(metadata.symbol == SYMBOL)
val cap = metadata.maxSupply?.let(::BigInteger)
require(cap == null || initialSupply.value <= cap) { "Initial supply exceeds cap" }
val pointer = content.put(metadata.encode()) val pointer = content.put(metadata.encode())
state.put(METADATA, pointer.value.encodeToByteArray()) state.put(METADATA, pointer.value.encodeToByteArray())
state.put(SUPPLY, initialSupply.value.toByteArray()) state.put(SUPPLY, initialSupply.value.toByteArray())
cap?.let { state.put(CAP, it.toByteArray()) }
state.put(PAUSED, byteArrayOf(0))
state.put(OWNER, owner.value.encodeToByteArray())
state.put(DEPLOYED, byteArrayOf(1)) state.put(DEPLOYED, byteArrayOf(1))
if (initialSupply.value.signum() > 0) token.mint(owner, initialSupply) if (initialSupply.value.signum() > 0) token.mint(owner, initialSupply)
pointer pointer
@ -109,19 +96,11 @@ class TwistoToken(
fun totalSupply(): Amount = Amount(state.get(SUPPLY)?.let(::BigInteger) ?: BigInteger.ZERO) fun totalSupply(): Amount = Amount(state.get(SUPPLY)?.let(::BigInteger) ?: BigInteger.ZERO)
fun cap(): Amount? = state.get(CAP)?.let { Amount(BigInteger(it)) }
fun paused(): Boolean = state.get(PAUSED)?.firstOrNull() == 1.toByte()
fun mint(caller: Address, recipient: Address, amount: Amount) = fun mint(caller: Address, recipient: Address, amount: Amount) =
synchronized(this) { synchronized(this) {
require(caller == owner) { "Only the owner may mint" } require(caller == owner) { "Only the owner may mint" }
check(state.get(DEPLOYED) != null) { "Twisto is not deployed" } check(state.get(DEPLOYED) != null) { "Twisto is not deployed" }
check(!paused()) { "Twisto transfers are paused" }
require(amount.value.signum() > 0) require(amount.value.signum() > 0)
require(cap()?.value?.let { totalSupply().value + amount.value <= it } ?: true) {
"Mint exceeds cap"
}
token.mint(recipient, amount) token.mint(recipient, amount)
state.put(SUPPLY, (totalSupply().value + amount.value).toByteArray()) state.put(SUPPLY, (totalSupply().value + amount.value).toByteArray())
} }
@ -129,50 +108,7 @@ class TwistoToken(
override fun balanceOf(owner: Address): Amount = token.balanceOf(owner) override fun balanceOf(owner: Address): Amount = token.balanceOf(owner)
override fun transfer(from: Address, to: Address, amount: Amount): Boolean = override fun transfer(from: Address, to: Address, amount: Amount): Boolean =
synchronized(this) {
check(state.get(DEPLOYED) != null) { "Twisto is not deployed" }
check(!paused()) { "Twisto transfers are paused" }
token.transfer(from, to, amount) token.transfer(from, to, amount)
}
fun burn(caller: Address, amount: Amount): Boolean =
synchronized(this) {
check(!paused()) { "Twisto transfers are paused" }
if (!token.burn(caller, amount)) return false
state.put(SUPPLY, (totalSupply().value - amount.value).toByteArray())
true
}
fun approve(owner: Address, spender: Address, amount: Amount) =
synchronized(this) {
check(!paused()) { "Twisto transfers are paused" }
require(amount.value.signum() >= 0)
state.put(allowanceKey(owner, spender), amount.value.toByteArray())
}
fun allowance(owner: Address, spender: Address): Amount =
Amount(state.get(allowanceKey(owner, spender))?.let(::BigInteger) ?: BigInteger.ZERO)
fun transferFrom(spender: Address, from: Address, to: Address, amount: Amount): Boolean =
synchronized(this) {
check(!paused()) { "Twisto transfers are paused" }
val approved = allowance(from, spender).value
if (amount.value.signum() <= 0 || approved < amount.value || !token.transfer(from, to, amount)) return false
state.put(allowanceKey(from, spender), (approved - amount.value).toByteArray())
true
}
fun pause(caller: Address) = requireOwner(caller) { state.put(PAUSED, byteArrayOf(1)) }
fun unpause(caller: Address) = requireOwner(caller) { state.put(PAUSED, byteArrayOf(0)) }
fun transferOwnership(caller: Address, nextOwner: Address) =
requireOwner(caller) {
require(nextOwner.value.isNotBlank())
state.put(OWNER, nextOwner.value.encodeToByteArray())
}
fun owner(): Address = state.get(OWNER)?.decodeToString()?.let(::Address) ?: owner
fun contractIr(): ContractIr = fun contractIr(): ContractIr =
contract("Twisto") { contract("Twisto") {
@ -191,22 +127,6 @@ class TwistoToken(
storageLoad() storageLoad()
returnWord() returnWord()
} }
function("approve", 4) {
storageStore()
returnWord()
}
function("transferFrom", 5) {
storageStore()
returnWord()
}
function("burn", 6) {
storageStore()
returnWord()
}
function("pause", 7) {
storageStore()
returnWord()
}
} }
private companion object { private companion object {
@ -214,15 +134,5 @@ class TwistoToken(
val DEPLOYED = "twisto:deployed".encodeToByteArray() val DEPLOYED = "twisto:deployed".encodeToByteArray()
val METADATA = "twisto:metadata".encodeToByteArray() val METADATA = "twisto:metadata".encodeToByteArray()
val SUPPLY = "twisto:supply".encodeToByteArray() val SUPPLY = "twisto:supply".encodeToByteArray()
val CAP = "twisto:cap".encodeToByteArray()
val PAUSED = "twisto:paused".encodeToByteArray()
val OWNER = "twisto:owner".encodeToByteArray()
fun allowanceKey(owner: Address, spender: Address) =
"twisto:allowance:${owner.value}:${spender.value}".encodeToByteArray()
}
private inline fun requireOwner(caller: Address, action: () -> Unit) = synchronized(this) {
require(caller == owner()) { "Only the owner may perform this action" }
action()
} }
} }

View file

@ -5,7 +5,6 @@ import rip.crit.twist.bips.FileStorageContract
import rip.crit.twist.bytecode.TwistBytecode import rip.crit.twist.bytecode.TwistBytecode
import rip.crit.twist.compiler.TwistCompiler import rip.crit.twist.compiler.TwistCompiler
import rip.crit.twist.compiler.LispIrCompiler import rip.crit.twist.compiler.LispIrCompiler
import rip.crit.twist.compiler.TwistMlCompiler
import rip.crit.twist.compiler.contract import rip.crit.twist.compiler.contract
import rip.crit.twist.core.Address import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount import rip.crit.twist.core.Amount
@ -45,8 +44,6 @@ import rip.crit.twist.tvm.Calldata
import rip.crit.twist.tvm.TransientStorage import rip.crit.twist.tvm.TransientStorage
import rip.crit.twist.tvm.Word256 import rip.crit.twist.tvm.Word256
import rip.crit.twist.tvm.BytecodeVirtualMachine import rip.crit.twist.tvm.BytecodeVirtualMachine
import rip.crit.twist.tvm.ExecutionContext
import rip.crit.twist.tvm.StateExecutionHost
import rip.crit.twist.bytecode.Instruction import rip.crit.twist.bytecode.Instruction
import rip.crit.twist.bytecode.OpCode import rip.crit.twist.bytecode.OpCode
import rip.crit.twist.p2p.PeerNetwork import rip.crit.twist.p2p.PeerNetwork
@ -385,47 +382,6 @@ class AppTest {
assertFailsWith<IllegalArgumentException> { LispIrCompiler().compile(source, "mallory") } assertFailsWith<IllegalArgumentException> { LispIrCompiler().compile(source, "mallory") }
} }
@Test
fun twistMlLowersFunctionalExpressionsToExecutableBytecode() {
val source =
"""
contract Arithmetic {
fun max(left, right) = if left > right then left else right;
fun stored(key) = let value = load(key) in value + 1;
}
""".trimIndent()
val contract = TwistMlCompiler().compile(source)
assertEquals(listOf("max", "stored"), contract.functions.map { it.name })
val input =
Word256.fromLong(1).toBytes() +
Word256.fromLong(3).toBytes() +
Word256.fromLong(7).toBytes()
val result = BytecodeVirtualMachine().execute(contract.bytecode(), input, Address("ml"))
assertTrue(result.success)
assertEquals(BigInteger.valueOf(7), Word256.fromBytes(result.output).toBigInteger())
}
@Test
fun twistoTwistMlContractInitializesAndTransfersOnTheVm() {
val contract = TwistMlCompiler().compile(Files.readString(Path.of("protocols/tokens/twisto.twml")))
val code = contract.bytecode()
val state = InMemoryWorldState()
val address = Address("twisto")
val owner = Address("owner")
fun call(caller: Address, selector: Long, vararg arguments: Word256) =
BytecodeVirtualMachine().execute(
code,
Word256.fromLong(selector).toBytes() + arguments.fold(byteArrayOf()) { bytes, word -> bytes + word.toBytes() },
StateExecutionHost(ExecutionContext(address, caller, Amount(BigInteger.ZERO)), state),
)
assertTrue(call(owner, 1, Word256.fromLong(99), Word256.fromLong(10), Word256.fromLong(10)).success)
val recipient = Word256.fromBytes(Sha256.bytes("recipient".encodeToByteArray()))
assertTrue(call(owner, 6, recipient, Word256.fromLong(4)).success)
val ownerKey = Word256.fromBytes(Sha256.bytes(owner.value.encodeToByteArray()))
assertEquals(BigInteger.valueOf(6), Word256.fromBytes(call(owner, 5, ownerKey).output).toBigInteger())
assertEquals(BigInteger.valueOf(4), Word256.fromBytes(call(owner, 5, recipient).output).toBigInteger())
}
@Test @Test
fun didMethodsResolveDocuments() { fun didMethodsResolveDocuments() {
val publicKey = ByteArray(32) { it.toByte() } val publicKey = ByteArray(32) { it.toByte() }
@ -504,25 +460,6 @@ class AppTest {
} }
} }
@Test
fun twistoSupportsAllowancesBurningPausesAndSupplyCaps() {
val owner = Address("owner")
val alice = Address("alice")
val bob = Address("bob")
val token = TwistoToken(owner, InMemoryKeyValueStore(), FileStorageContract(Files.createTempDirectory("twisto-token")))
token.deploy(TwistoMetadata(description = "Capped", maxSupply = "20"), Amount(BigInteger.TEN))
token.approve(owner, alice, Amount(BigInteger.valueOf(4)))
assertTrue(token.transferFrom(alice, owner, bob, Amount(BigInteger.valueOf(4))))
assertEquals(BigInteger.ZERO, token.allowance(owner, alice).value)
assertTrue(token.burn(bob, Amount(BigInteger.ONE)))
assertEquals(BigInteger.valueOf(9), token.totalSupply().value)
token.pause(owner)
assertFailsWith<IllegalStateException> { token.transfer(owner, bob, Amount(BigInteger.ONE)) }
token.unpause(owner)
token.mint(owner, owner, Amount(BigInteger.valueOf(11)))
assertFailsWith<IllegalArgumentException> { token.mint(owner, owner, Amount(BigInteger.ONE)) }
}
private fun hex(value: String): ByteArray = value.chunked(2).map { it.toInt(16).toByte() }.toByteArray() private fun hex(value: String): ByteArray = value.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
private fun ByteArray.hex(): String = joinToString("") { "%02x".format(it) } private fun ByteArray.hex(): String = joinToString("") { "%02x".format(it) }
} }

View file

@ -4,18 +4,15 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import rip.crit.twist.compiler.TwistCompiler import rip.crit.twist.compiler.TwistCompiler
import rip.crit.twist.compiler.LispIrCompiler import rip.crit.twist.compiler.LispIrCompiler
import rip.crit.twist.compiler.TwistMlCompiler
fun main(args: Array<String>) { fun main(args: Array<String>) {
require(args.size in 2..3) { require(args.size in 2..3) {
"Usage is twistc <source.twistasm|source.twistl|source.twml> <output.tbc> [principal]" "Usage is twistc <source.twistasm|source.twistl> <output.tbc> [principal]"
} }
val source = Path.of(args[0]) val source = Path.of(args[0])
val text = Files.readString(source) val text = Files.readString(source)
val bytecode = val bytecode =
if (source.fileName.toString().endsWith(".twml")) { if (source.fileName.toString().endsWith(".twistl")) {
TwistMlCompiler().compile(text).bytecode()
} else if (source.fileName.toString().endsWith(".twistl")) {
LispIrCompiler().compile(text, args.getOrNull(2) ?: "anonymous") LispIrCompiler().compile(text, args.getOrNull(2) ?: "anonymous")
} else { } else {
TwistCompiler().compile(text) TwistCompiler().compile(text)

View file

@ -1,31 +1,31 @@
package rip.crit.twist.tool.twisto package rip.crit.twist.tool.twisto
import java.math.BigInteger import java.math.BigInteger
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import rip.crit.twist.bips.FileStorageContract import rip.crit.twist.bips.FileStorageContract
import rip.crit.twist.compiler.TwistMlCompiler import rip.crit.twist.core.Address
import rip.crit.twist.core.Amount
import rip.crit.twist.store.FileKeyValueStore
import rip.crit.twist.twisto.TwistoMetadata import rip.crit.twist.twisto.TwistoMetadata
import rip.crit.twist.tvm.Word256 import rip.crit.twist.twisto.TwistoToken
fun main(args: Array<String>) { fun main(args: Array<String>) {
require(args.size in 1..3) { require(args.size in 1..3) {
"Usage is twisto <data-folder> [owner-label] [initial-supply]" "Usage is twisto <data-folder> [owner] [initial-supply]"
} }
val root = Path.of(args[0]) val root = Path.of(args[0])
val owner = args.getOrNull(1) ?: "twisto-owner" val owner = Address(args.getOrNull(1) ?: "twisto-owner")
val supply = BigInteger(args.getOrNull(2) ?: "1000000") val token =
val metadata = FileStorageContract(root.resolve("metadata")) TwistoToken(
val pointer = metadata.put(TwistoMetadata(description = "Named token on the Twist network").encode()) owner,
val source = checkNotNull(object {}.javaClass.getResource("/twisto.twml")) { "Missing Twisto contract source" }.readText() FileKeyValueStore(root.resolve("state")),
val code = TwistMlCompiler().compile(source).bytecode() FileStorageContract(root.resolve("metadata")),
Files.createDirectories(root) )
Files.write(root.resolve("twisto.tbc"), code) val pointer =
val initialization = token.metadataPointer()
Word256.fromLong(1).toBytes() + ?: token.deploy(
Word256.fromBytes(pointer.value.chunked(2).map { it.toInt(16).toByte() }.toByteArray()).toBytes() + TwistoMetadata(description = "Named token on the Twist network"),
Word256.of(supply).toBytes() + Amount(BigInteger(args.getOrNull(2) ?: "1000000")),
Word256.of(supply).toBytes() )
Files.write(root.resolve("initialize.calldata"), initialization) println("deployed symbol=${token.symbol} metadata=${pointer.value} owner=${owner.value}")
println("compiled contract=${root.resolve("twisto.tbc")} metadata=${pointer.value} owner-label=$owner")
} }

View file

@ -1,10 +0,0 @@
contract Twisto {
fun initialize(metadata, supply, cap) = if load(0) == 0 then let ignored = store(0, caller) in let ignored = store(1, metadata) in let ignored = store(2, cap) in let ignored = store(3, supply) in store(caller, supply) else 0;
fun metadata() = load(1);
fun total_supply() = load(3);
fun cap() = load(2);
fun balance_of(account) = load(account);
fun transfer(to, amount) = let from = caller in let balance = load(from) in if amount > balance then 0 else let ignored = store(from, balance - amount) in store(to, load(to) + amount);
fun mint(to, amount) = let owner = load(0) in let supply = load(3) in let ceiling = load(2) in if caller == owner then if ceiling < supply + amount then 0 else let ignored = store(3, supply + amount) in store(to, load(to) + amount) else 0;
fun burn(amount) = let account = caller in let balance = load(account) in let supply = load(3) in if amount > balance then 0 else let ignored = store(account, balance - amount) in store(3, supply - amount);
}

View file

@ -1,40 +0,0 @@
contract Twisto {
fun initialize(metadata, supply, cap) =
if load(0) == 0 then
let ignored = store(0, caller) in
let ignored = store(1, metadata) in
let ignored = store(2, cap) in
let ignored = store(3, supply) in
store(caller, supply)
else 0;
fun metadata() = load(1);
fun total_supply() = load(3);
fun cap() = load(2);
fun balance_of(account) = load(account);
fun transfer(to, amount) =
let from = caller in
let balance = load(from) in
if amount > balance then 0 else
let ignored = store(from, balance - amount) in
store(to, load(to) + amount);
fun mint(to, amount) =
let owner = load(0) in
let supply = load(3) in
let ceiling = load(2) in
if caller == owner then
if ceiling < supply + amount then 0 else
let ignored = store(3, supply + amount) in
store(to, load(to) + amount)
else 0;
fun burn(amount) =
let account = caller in
let balance = load(account) in
let supply = load(3) in
if amount > balance then 0 else
let ignored = store(account, balance - amount) in
store(3, supply - amount);
}