NEAR Revisión de contrato en vivo | Parte 2: Contrato de grupo de participación (Staking)

28 min read
To Share and +4 nLEARNs

Introducción

Hola, soy Eugene y esta es la segunda parte de la serie de revisión de contratos en vivo y hoy vamos a revisar el contrato del grupo de participación (o Staking), que se utiliza en este momento para asegurar el sistema de prueba de participación del Protocolo NEAR. Básicamente, todos los validadores que se ejecutan actualmente en el protocolo NEAR se ejecutan por medio de este contrato. Ellos no controlan la cuenta que bloquea la cantidad de tokens NEAR necesarios para la prueba de la participación por sí mismos, sino que el contrato apuesta la cantidad, y solo proporcionan un grupo de staking y ejecutan sus nodos. Hoy vamos a profundizar en este contrato. Entre los contratos principales, tenemos un contrato de grupo de staking y es un poco más complicado que el contrato anterior que revisamos (el contrato de votación). Así que hoy nos vamos a centrar más en la lógica y menos en near_bindgen y las cosas específicas de Rust, pero probablemente involucrará un poco más de conocimiento de NEAR Protocol. Aquí está el contrato del grupo de staking en el Github de NEAR. A continuación te muestro el video original en el que se basa esta guía.

lib.rs

Estructura del Constructor

Como antes, el contrato parte de la estructura principal. En este caso, se trata de una estructura de contrato de participación. Como puedes ver, hay near_bindgen, BorshSerialize y BorshDeserialize. La estructura ahora tiene muchos más campos que la anterior, y hay algunos comentarios sobre ellos, la mayoría de ellos probablemente estén actualizados. La lógica del contrato del grupo de participación nos permite hacer lo siguiente: básicamente, cualquiera puede depositar una cantidad de tokens NEAR en el grupo de participación y delegarlos en el grupo de participación bloqueandolos en el grupo. Eso nos permite agrupar los saldos de varias personas (aquí las llamamos cuentas) en una gran participación, y de esta manera, este gran grupo puede calificar para puestos de validación. NEAR Protocol tiene una cantidad limitada de asientos para un solo fragmento en este momento, hay como máximo 100 asientos de validación. Puedes pensar en los asientos de la siguiente manera: si tomas la cantidad total de tokens apostados y la divides por 100, el resultado será la cantidad mínima de tokens requeridos para un solo asiento, excepto que es un poco más complicado de implicar la eliminación las apuestas que no están calificadas para contener esta cantidad mínima, etc. Este contrato es básicamente un contrato independiente sin ninguna clave de acceso que esté controlada por el propietario. En este caso, el propietario se proporciona en el método de inicialización.

Método de inicialización

Así que vayamos al método de inicialización. Tienes tres argumentos y el primero es el owner_id, que es el ID de la cuenta del propietario. El propietario tiene varios permisos en este contrato que le permiten realizar acciones que no están disponibles para el resto de las cuentas. Uno de estos métodos es votar en nombre del grupo de participación en el contrato de votación que discutimos la última vez. Entonces el propietario puede llamar al método de voto.

Luego verificamos que el predecesor (predecessor_account_id) sea igual al propietario, ya que este método solo puede ser llamado por el propietario.

Entonces, lo que hace el método de votación es que verifica que el método fue llamado por el propietario y luego verifica alguna lógica, pero eso no es importante en este momento.

Entonces, el contrato es el propietario, y este propietario puede hacer ciertas cosas, tiene permisos adicionales. Luego, se necesitan algunos campos más: la stake_public_key. Cuando haces staking en NEAR, debes proporcionar una clave pública que será utilizada por tu nodo validador para firmar mensajes en nombre del nodo validador. Esta clave pública puede ser diferente de cualquier clave de acceso, e idealmente debería ser diferente de cualquier clave de acceso porque tu nodo puede ejecutarse en un centro de datos que puede ser vulnerable a algunos ataques. En este caso, lo máximo que pueden hacer es hacer algo malo en la red, pero no en tu cuenta. No pueden robar tus fondos, y puedes reemplazar fácilmente esta clave en comparación con la forma en que se reemplaza una clave de acceso defectuosa. Por último, el tercer argumento que toma el contrato es la reward_fee_fraction inicial. Esta es la comisión que toma el propietario del grupo de participación por ejecutar el nodo validador.

Esta es una fracción que tiene un numerador y un denominador, y le permite básicamente decir «Tomo el 1% de las recompensas por ejecutar este grupo en particular», por ejemplo. Supongamos que tiene 1 000 000 de tokens y ellos adquirieron alguna recompensa, digamos que hay una recompensa de 10 000 tokens, luego el propietario tomará el 1% de esto, que son 100 tokens. El tipo de dato Float tiene un comportamiento impredecible cuando se multiplica. Por ejemplo, con fracciones, puedes usar matemáticas con una mayor cantidad de bits. Entonces, la forma en que haces la división, por ejemplo, es primero multiplicar la cantidad que es u128 por un numerador (esto ya puede desbordarse en u128), pero es por eso que hacemos esto en u256. Luego lo divides por el denominador que debería llevarlo nuevamente por debajo de u128. Esto te da una mayor precisión que float64, que no puede funcionar con una precisión de u128 bits, por lo que tendrás algunos errores de redondeo o errores de precisión cuando hagas los cálculos. Por lo que, de otra manera necesitas float de mayor precisión, que no son realmente diferentes de las matemáticas donde simulamos esto con u256. Solidity originalmente no admitía tipo de dato Float, y nosotros tampoco lo admitimos originalmente, pero eso arrojó algunos problemas sobre el formato de cadenas en Rust para la depuración, por lo que decidimos que no hay nada de malo en admitir floats, especialmente porque estandarizamos esto en el lado de la máquina virtual. El mayor problema con los floats fue el comportamiento indefinido en torno a ciertos valores de cargas, por ejemplo, qué contienen otros bits cuando tienes un float infinito. Estandarizamos esto, y ahora son independientes de la plataforma equivalente. Por lo tanto, está bien usar float ahora en nuestro entorno de VM.

La práctica estándar con “init» es que primero verifiquemos que el estado no existe. Luego verificamos la entrada. Lo primero que hacemos es verificar que la fracción sea válida y comprobar que el denominador no es cero. A continuación, tenemos una declaración «else» que verifica que el numerador sea menor o igual que el denominador, lo que significa que la fracción es menor o igual que 1. Esto es importante para evitar algunos errores lógicos. Lo siguiente que hacemos es verificar que la cuenta sea válida. Este contrato se redactó antes de algunas de las métricas de ayuda que existen ahora. Por ejemplo, tenemos la identificación de cuenta válida en tipos JSON que hace esta verificación automáticamente durante la deserialización, si no es válida, entrará en pánico. Después de eso, extraemos el saldo actual de la cuenta del contrato de participación. Este saldo suele ser lo suficientemente grande porque tiene que pagar por el almacenamiento de este contrato en particular, y luego decimos que vamos a asignar algunos tokens para el STAKE_SHARE_PRICE_GUARANTEE_FUND. El pool de participación tiene ciertas garantías que son importantes para los contratos locales. Las garantías aseguran que cuando depositas en el grupo de staking, deberías poder retirar al menos la misma cantidad de tokens, y no puedes perder tokens ni siquiera por hasta 1000 000 000 000 de yoctoNEAR en este contrato al depositar y retirar de los grupos de staking. El fondo de STAKE_SHARE_PRICE_GUARANTEE_FUND es de alrededor de 1 billón (“1 trillion” en inglés) de yoctoNEAR, mientras que normalmente consumimos alrededor de 1 o 2 billones de yoctoNEAR en errores de redondeo. Finalmente recordamos cuál es el saldo que vamos a bloquear en nombre de este contrato. Esto es necesario para establecer una línea de base para limitar las diferencias de redondeo. A continuación, verificamos que la cuenta aún no se haya bloqueado. Esto podría romper algo de lógica, pero no queremos que esto suceda, por lo que queremos inicializar el contrato antes de bloquear algo. Finalmente, inicializamos la estructura, pero no la devolvemos de inmediato. Acabamos de crear la estructura: StakingContract.

Nota: Un epoch es una unidad de tiempo de aproximadamente ~12 horas o 43,200 segundos para ser exactos.

Luego emitimos una transacción de re-delegación(re-staking). Esto es importante, porque debemos asegurarnos de que esta clave de staking que se proporcionó sea una clave restringida de ristretto válida, por ejemplo, una clave válida 5 119. Hay algunas claves en la curva que son claves válidas, pero no son específicas de ristretto, y las claves de validación solo pueden ser específicas de ristretto. Esto es algo específico de NEAR Protocol, y lo que sucede es que realiza una transacción de participación con la clave dada. Una vez que se crea esta transacción a partir del contrato, validamos esta transacción cuando sale. Si la clave no es válida, arrojará un error y fallará la inicialización completa de este grupo de participación. Si pasa una stake_public_key no válida como entrada, se revertirá la consolidación e implementación de tu contrato, y todo lo que suceda en este lote de transacciones se revertirá. Esto es importante para que el grupo no tenga una clave no válida, porque eso podría permitirle bloquear la participación de otras personas. Como parte de las garantías, decimos que si quitas tu participación, tus tokens se devolverán en 4 epochs. Serán elegibles para el retiro, y esto es importante para poder devolverlos a tu wallet.

Creo que son demasiados detalles antes de explicar la descripción general de alto nivel de cómo funcionan los contratos y cómo funcionan los saldos. Expliquemos el concepto de cómo podemos distribuir recompensas a los propietarios de cuentas en un tiempo constante cuando pasa un epoch. Esto es importante para la mayoría de los contratos inteligentes. Se quiere que actúen en un tiempo constante para cada método en lugar de un tiempo lineal para la cantidad de usuarios, porque si la cantidad de usuarios aumenta, la cantidad de gas necesaria para operar una escala lineal también aumentará, y eventualmente se acabará el gas. Es por eso que todos los contratos inteligentes deben actuar en un tiempo constante.

Estructura de la cuenta

La forma en que funciona para cada usuario está en la estructura denominada account. Cada usuario que ha delegado a este grupo de participación tendrá una estructura llamada account que tiene los siguientes campos: unstaked es el saldo en yoctoNEAR que no está delegado, por lo que es solo el saldo del usuario. Entonces stake_shares es en realidad un saldo, pero no en base NEAR, sino en el número de participaciones compartido. stake_shares es un concepto que se agregó a este grupo de participación en particular. La forma en que funciona es cuando tú delegas tus NEAR, esencialmente compras nuevas acciones al precio actual al convertir tu saldo no invertido en acciones de participación. El precio de una acción de participación es originalmente 1, pero con el tiempo crece con las recompensas, y cuando la cuenta recibe recompensas su saldo total de participación aumenta, pero la cantidad total de acciones de participación no cambia. Básicamente, cuando una cuenta recibe recompensas de validación o algunos otros depósitos directamente al saldo, aumenta la cantidad que puede recibir por cada acción de participación. Supongamos, por ejemplo, que originalmente tenías 1 millón NEAR depositado en esta cuenta. Supongamos que obtienes 1 millón de acciones (ignorando el yoctoNEAR por ahora), si el grupo de participación recibió 10000 NEAR en recompensas, todavía tienes 1 millón de acciones, pero el millón de acciones ahora corresponde a 1 010 000 NEAR. Ahora, si alguien más quiere bloquear NEAR en este momento, comprará acciones de participación internamente dentro del contrato al precio de 1.001 NEAR, porque cada acción vale eso ahora. Cuando recibes otra recompensa, no necesitas comprar más acciones a pesar del saldo total, y en el tiempo constante todos comparten la recompensa proporcionalmente a la cantidad de acciones que tienen. Ahora, cuando quieres retirar tus NEAR bloqueados, básicamente estás vendiendo estas acciones o quemándolas utilizando el concepto de tokens fungibles a favor del saldo no bloqueado. Por lo tanto, vendes al precio actual, disminuye la cantidad total de participación, así como la cantidad total de acciones, y cuando compras, aumenta el saldo total de la participación y la participación total de la acción mientras mantiene el precio constante. Cuando delegas o dejas de delegar no cambias el precio, cuando recibes las recompensas aumentas el precio.

El precio solo puede subir, y esto puede dar lugar a errores de redondeo cuando tu yoctoNEAR y tu saldo no pueden corresponder con precisión. Es por eso que tenemos este fondo de garantía de 1 billón de yoctoNEAR que arrojará un yoctoNEAR adicional a la mezcla varias veces. Finalmente, la parte final está ahí, debido a que NEAR Protocol no retira los NEAR y los devuelve de inmediato, tienes que esperar tres epoch hasta que tu saldo quede como no delegado y sea devuelto a la cuenta. Si dejas de participar, no puedes retirar este saldo inmediatamente del grupo de participación, sino que debes esperar tres epoch. Entonces recuerdas a qué altura de epoch llamaste a la acción unstake (dejaste de participar), y después de tres epoch tu saldo se desbloqueará y deberías poder retirar tus acción sin delegar. Sin embargo, hay una advertencia: si llamas a unstake en el último bloque del epoch, la promesa real de hacer unstaking (retirar tus NEAR) llegará para la próxima epoch. Llegará al primer bloque de la próxima epoch, y eso retrasará tu saldo bloqueado para desbloquearse dentro de cuatro epoch en lugar de tres. Esto se debe a que registramos la epoch en el bloque anterior, pero la transacción real ocurrió en el siguiente bloque, en la siguiente epoch. Para asegurarnos de que eso no suceda, bloqueamos el saldo por cuatro epoch en lugar de tres para tomar en cuenta este caso fronterizo. Eso es lo que constituye una cuenta. La idea de las acciones no es tan nueva, porque en Ethereum la mayoría de los proveedores de liquidez y los creadores de mercado automatizados utilizan un concepto similar. Cuando, por ejemplo, depositas en el grupo de liquidez, obtienes algún tipo de token de este grupo en lugar de la cantidad real que se representa allí. Cuando te retiras del grupo de liquidez, se quema este token y obtienes los tokens representados reales. La idea es muy similar a llamarlas acciones, porque tienen un precio correspondiente y podríamos haberlas llamado de otra manera. Esto fue casi desde el comienzo de este contrato de grupo de participación. Se exploró cómo podemos hacer esto correctamente, y una forma era limitar la cantidad de cuentas que pueden depositar en una cuenta de grupo determinada para esta actualización en particular. Eventualmente aterrizamos en el tiempo de complejidad constante y eso era un modelo más simple. Luego, las matemáticas de la estructura de stake_share se volvieron algo razonables, incluso así que también hay algunas involucradas allí.

Tipos de contrato

Repasemos este contrato. No está tan bien estructurado como un contrato de bloqueo, por ejemplo, porque el bloqueo es aún más complicado. Los tipos todavía están agrupados en el mismo contrato. Hay un montón de tipos de tipos, por ejemplo reward_fee_fraction es un tipo separado.

La cuenta es un tipo separado y también hay una cuenta legible por humanos que también es un tipo que solo se usa en llamadas para ver, por lo que no se usa para la lógica internamente.

Luego, después de terminar con todos los tipos, tenemos llamadas de contrato cruzado utilizando una interfaz de alto nivel.

Hay dos de ellos. La forma en que funciona es que tiene una macro de near_bindgen llamada ext_contract (que significa contrato externo). Puedes darle un nombre corto que generará y que podrás usar. Luego, tienes una descripción del rasgo que describe la interfaz del contrato externo que deseas utilizar. Esto describe el hecho de que puedes llamar a un método de voto en un contrato remoto y pasar un argumento. El argumento is_vote, que es un booleano verdadero o falso. Ahora podrás crear una promesa cuando la necesites y pasar un argumento posicional en lugar de un argumento serializado JSON. La macro se convertirá en apis de promesa de bajo nivel detrás de escena. La segunda interfaz es para una devolución de llamada en nosotros mismos, esto es bastante común, puedes llamarlo ext_self. Cuando necesites hacer una devolución de llamada y hacer algo sobre el resultado de la promesa asincrónica, puedes tener este tipo de interfaz. Lo que hacemos es comprobar si la acción de bloqueo se realizó correctamente. Finalmente, tenemos el cuerpo de la estructura de implementación principal del grupo de participación.

Estructura de archivo de contrato

Este contrato se divide en varios módulos.

Tienes libs.rs, que es la entrada principal, y también tienes un módulo interno. El módulo interno tiene la implementación sin la macro near_bindgen, por lo que ninguno de estos métodos será visible para ser llamado por un contrato por otra persona en la cadena. Solo se pueden llamar dentro de este contrato internamente para que no generen formatos JSON y no deserialicen el estado. Todos actúan como métodos habituales del lenguaje rust. El funcionamiento de este contrato de alto nivel es que cuando pasa una epoch puedes adquirir ciertas recompensas como validador.

Métodos importantes del contrato

Tenemos un método de ping que comprueba con un ping el contrato. El método ping comprueba si ha pasado un epoch y luego necesitamos distribuir recompensas. Si la epoch cambió, también se reiniciará, porque podría haber algún cambio en la cantidad de participación total que tiene que bloquear el contrato. El siguiente es el depósito.

El método de depósito es payable, lo que significa que puede aceptar un depósito adjunto. Esto es similar al decorador de Ethereum que le permite recibir fondos solo de los métodos que los esperan. Entonces near_bindgen por defecto entrará en pánico si intenta llamar a un método, por ejemplo ping, y adjuntar un depósito a este método. En consecuencia, payable nos permite adjuntar depósitos. En cada método hay un ping interno para asegurarnos de que distribuimos las recompensas anteriores antes de cambiar cualquier lógica. La estructura común es que si necesitamos hacer re-staking, primero hacemos algo de lógica y ya luego se hace re-staking.

El siguiente método es deposit_and_stake. Esta es una combinación de dos métodos. Primero, deposita el balance en el balance bloqueado de tu cuenta y también bloquea la misma cantidad inmediatamente en lugar de realizar dos transacciones. También es payable, porque también acepta un depósito.

El siguiente es withdraw_all. Intenta retirar todo el saldo pendiente de la cuenta que lo llamó. Cuando interactúa con el grupo de participación, debe interactuar con la cuenta que posee el saldo. En este caso, este es el predecessor_account_id y básicamente verificamos la cuenta, y luego retiramos la cantidad no bloqueada si podemos. Si no se retira, entrará en pánico. Por ejemplo, si todavía está bloqueado debido a que se quitó el bloqueo hace menos de 4 epoch.

Withdraw te permite retirar solo un saldo parcial.

Luego, stake_all bloquea todo el saldo no bloqueado, aunque es bastante raro usar este método, porque normalmente usas bloqueo de depósito, y ya tienes todo el saldo bloqueado.

Luego, en el método stake, solo bloqueas una cierta cantidad del balance bloqueado. La billetera Moonlight usa un costo separado para depositar el bloqueo, pero usan una transacción por lotes para hacer esto.

Finalmente, tienes unstake_all, que básicamente retira (desbloquea) todas tus acciones de participación convirtiéndolas a yoctoNEAR. Existe un método auxiliar que convierte tu número de acciones a una cantidad de yoctoNEAR y redondea hacia abajo, porque no podemos darte un extra por tu acción multiplicada por el precio. Así es como obtenemos la cantidad y luego llamamos a unstake por la cantidad dada.

La lógica staked_amount_from_num_shares_rounded_down usa u256, porque los saldos operan en u128. Para evitar que se desborde, multiplicamos el total_staked_balance por el número de acciones en u256. El precio es el cociente redondeado a la baja.

La versión redondeada staked_amount_from_num_shares_dered_up es muy similar excepto que hacemos una verificación que nos permite redondear. Al final de ambos lo volvemos a convertir a u128.

Luego tenemos la acción unstake que es muy similar a unstake_all, excepto que pasas la cantidad.

Métodos Getter/View

Después de eso, hay un montón de métodos getter que son llamadas de vista que le devuelven algunas cantidades. Puedes obtener el saldo no bloqueado de la cuenta, el saldo bloqueado de la cuenta, el saldo total de la cuenta, verificar si puedes retirar, el saldo total del bloqueo, que es la cantidad total que el grupo de participación tiene el bloqueo activo.

Luego puedes saber quién es el propietario del grupo de participación, puedes obtener la tarifa de recompensa actual o la comisión del grupo de participación, obtener la clave de participación actual y hay una cosa separada que verifica si el propietario pausó el grupo de participación.

Supongamos que el propietario realiza una migración en el grupo de participación en el nodo. Necesitan retirarse(desbloquearse) completamente, así que, por ejemplo, pueden pausar el grupo de participación que entonces enviará una transacción de estado a NEAR Protocol, y luego no hará re-staking hasta que reanuden el grupo de participación. Sin embargo, aún puedes retirar tus saldos, pero dejarás de adquirir recompensas una vez que haya pasado.

Finalmente, puedes obtener una cuenta legible por humanos que te brinda cuántos tokens tienes realmente para la cantidad de acciones al precio actual, y finalmente dice si puedes retirar o no.

accounts.len() te da la cantidad de cuentas, que es la cantidad de delegadores a este grupo de participación, y también puedes recuperar la información de múltiples delegadores a la vez. Esta es la paginación en una gran cantidad de cuentas dentro del mapa desordenado. Una forma de hacer esto es usar la función ayudante que llamamos keys_as_vector del mapa desordenado. Te brinda una colección persistente de claves del mapa, y luego puedes usar un iterador para solicitar cuentas de estas claves. Esa no es la forma más eficiente, pero te permite implementar la paginación en mapas desordenados.

Métodos Owner (de propietario)

Hay varios métodos de owner. Un método owner es un método que solo puede llamar el propietario. El propietario puede actualizar la clave de bloqueo. Supongamos que tienen un nodo diferente y que el propietario necesita usar una clave diferente. Todos estos métodos primero verifican que solo el propietario pueda llamarlo.

Este es el método que cambia la comisión en el grupo de apuestas. El propietario puede cambiar la comisión que estará activa en esta epoch empezando en esta epoch inmediatamente, pero todas las comisiones anteriores se calcularán utilizando la tarifa anterior.

Entonces este fue el método de votación que nos permitió hacer la transición a la fase dos de la red principal(MainNet).

A continuación, se encuentran los dos métodos que ya describí y que permiten pausar el bloqueo y reanudarlo.

El resto son solo pruebas. La mayor parte de la lógica ocurre en el interior.

Prueba de simulación

Básicamente, también tenemos pruebas de simulación para un grupo en particular. Esta prueba de simulación muestra cómo va a funcionar realmente la red. Primero inicializamos el grupo.

Bob es el delegador. Bob llamó al método de depósito del grupo, que es deposit_amount, utilizando el método de depósito. Luego, Bob puede verificar que el saldo no bloqueado esté funcionando correctamente. Entonces Bob bloquea la cantidad. Luego verificamos la cantidad bloqueada ahora. Verificamos que Bob ha bloqueado la misma cantidad.

Bob llama al método ping. No hay recompensas, pero en las simulaciones, las recompensas no funcionan de todos modos, por lo que debe hacerlo manualmente. Verificaremos una vez más que la cantidad de Bob sigue siendo la misma. Entonces el grupo se reanuda. Verificamos que el grupo se haya reanudado y luego lo bloqueamos a cero. Luego simulamos que el grupo ha adquirido algunas recompensas (1 NEAR) y Bob hace ping al grupo. Luego verificamos que la cantidad que recibió Bob sea positiva. Ese es un caso de simulación muy simple que dice que Bob depositó primero en el grupo, lo que verifica que la pausa y la reanudación funcionen, o simula que funciona y se asegura de que en el grupo no se haga staking(no sé bloqueé más) mientras está en pausa. Luego, cuando se reanuda, el grupo realmente está participando bloqueando. Entonces, esta prueba verifica no sólo esto, sino también que Bob ha obtenido la recompensa y qué se ha distribuido la recompensa. Hay otra prueba que verifica algo de lógica, pero es más complicada. Hay algunas pruebas unitarias en la parte inferior de esto que se supone que verifican ciertas cosas.

Algunas de estas pruebas no son ideales, pero verifican ciertas cosas que fueron lo suficientemente buenas como para asegurarse de que las matemáticas se sumen.

internal.rs

Método de ping interno

Pasemos a internal_ping. Es el método al que cualquiera puede llamar mediante ping para asegurarse de que se distribuyen las recompensas. En este momento tenemos grupos de participación activos y hay una cuenta delegada por una de las personas de NEAR que básicamente hace ping a cada bloqueo en el grupo cada 15 minutos para asegurarse de que se hayan distribuido las recompensas para mostrarlas en el saldo. De esa forma funciona la distribución de recompensas. Primero verificamos la altura de la epoch actual, por lo que si la altura de la epoch es la misma, entonces la epoch no ha cambiado, devolvemos falso para que no sea necesario volver a hacer re-staking. Si la epoch ha cambiado, recordamos que existe la epoch actual (altura de la epoch), obtenemos el nuevo saldo total de la cuenta. Se puede llamar a Ping cuando algunos tokens se depositaron a través de boletas de depósito, y ya son parte de account_balance, y dado que se llamó a ping antes, debemos restar este saldo antes de distribuir las recompensas. Obtenemos la cantidad total que tiene la cuenta, incluido el saldo bloqueado y el saldo desbloqueado. El saldo bloqueado es una cantidad apostada que adquiere recompensas, y el saldo desbloqueado también puede tener recompensas en ciertos escenarios en los que disminuye tu cantidad bloqueada, pero tus recompensas aún se reflejarán durante las próximas dos epoch. Después de eso, llegarán a la cantidad no apostada. Verificamos usando assert! que el saldo total es mayor que el saldo total anterior. Esto es algo invariante que requiere el grupo de participación. Hubo un montón de cosas en la red de prueba que fallaron en este invariante porque las personas todavía tenían claves de acceso en el mismo grupo de participación, y cuando las tienes, gastas el saldo en gas y puedes disminuir tu saldo total sin adquirir la recompensa. Finalmente, calculamos la cantidad de recompensas que recibió el grupo de participación. Este es el saldo total menos el saldo total conocido anteriormente, entonces es el saldo de la epoch anterior. Si las recompensas son positivas las distribuimos. Lo primero que hacemos es calcular la recompensa que el propietario se lleva como comisión.

Multiplicamos reward_fee_fraction por la recompensa total recibida y esto se redondea de manera similar con el numerador en u256 multiplicado por el valor dividido por el denominador en u256.

Owners_fee es la cantidad en yoctoNEAR que el propietario se quedará. La recompensa restante son las recompensas restantes que se deben volver a bloquear. Luego vuelve a re-stake. El propietario recibió las recompensas en yoctoNEAR, no en acciones, pero debido a que toda la lógica tiene que estar en acciones, el propietario del grupo de participación compra acciones al precio de las distribuciones de recompensa posteriores al resto de los delegadores. Entonces num_shares es la cantidad de acciones que el propietario recibirá como compensación por administrar el grupo de participación. Si es positivo, aumentamos la cantidad de acciones y recuperamos la cuenta del propietario, y también aumentamos la cantidad total de participación en acciones. Si por alguna razón durante el redondeo a la baja este saldo se convirtió en cero, fue debido a que la recompensa fue muy pequeña y el precio por acción fue muy grande, y entonces el grupo sólo recibió cero de recompensas. En ese caso, este saldo solo irá al precio por acción en lugar de compensar al propietario. A continuación, colocamos algunos datos de registro totales que dicen que existe la epoch actual, que recibimos las recompensas en una cantidad de acciones o fichas en juego, que el saldo total de la apuesta del grupo es algo y registramos la cantidad de acciones. La única forma en que exponemos el número de acciones al mundo externo es a través de los registros. A continuación, si el propietario recibió recompensas, está diciendo que la recompensa total fueron tantas acciones. Por último, recordamos el nuevo saldo total y eso es todo. Hemos distribuido todas las recompensas en tiempo constante y solo actualizamos una cuenta (la cuenta del propietario) para la comisión, y sólo si la comisión fue positiva.

Método de participación interna

Internal_stake es donde implementamos el fondo de garantía de precio. Digamos que el predecesor, en este caso lo llamaremos account_id quiere bloquear una cantidad de tokens. En realidad, el balance no es un tipo JSON, porque es un método interno, por lo que no necesitamos JSON aquí. Calculamos cuántas acciones redondeadas a la baja se requieren para bloquear la cantidad dada, por lo que esta es la cantidad de acciones que recibirá el propietario, pero la cantidad tiene que ser positiva. Luego verificamos el monto que el propietario debe pagar por las acciones, nuevamente redondeado a la baja. Esto es para garantizar que cuando el propietario compró acciones y las volvió a convertir sin recompensas, nunca perdió el 1 yoctoNEAR, porque eso podría romper la garantía. Finalmente, afirmamos que la cuenta tiene suficiente para pagar el monto cobrado, y disminuimos el saldo interno no bloqueado y aumentamos el saldo interno del número de acciones de la cuenta. A continuación, redondeamos el staked_amount_from_num_shares_rounded_up para que el número de acciones se redondee al alza. Este 1 centavo extra o 1 yoctoNEAR extra procederá del fondo garantizado durante el redondeo de las acciones. Cobramos menos al usuario, pero contribuimos más a la cantidad de este 1 billón de yoctoNEAR que originalmente habíamos designado para esto. Esta diferencia generalmente es de 1 yoctoNEAR que puede provenir del redondeo hacia arriba o hacia abajo. Después de eso, está la cantidad de total_staked_balance y total_stake_shares. A continuación, acuñamos nuevas acciones con ellos. Finalmente ponemos un log y devolvemos el resultado.

Unstaking funciona de manera muy similar. Redondea a la alza la cantidad de acciones que debe pagar. Luego calculamos la cantidad que recibes, redondeando a la alza nuevamente para pagarte de más por esto. Esto también proviene de un fondo de garantía. Luego disminuimos las acciones para aumentar la cantidad e indicamos cuándo puedes desbloquear el saldo entre cuatro epoch. El monto de unstake_amount se redondea a la baja para que desembolsemos un poco menos para garantizar el precio de otros participantes del grupo. Así es básicamente cómo funciona el grupo de apuestas y cómo funcionan las matemáticas. Compensamos los errores de redondeo de los fondos que asignamos.

Conclusión

Actualizamos las claves de ristretto durante el diseño de este contrato y fue tan sorprendente que tenemos que dar cuenta de esto. En el STAKE_SHARE_PRICE_GUARANTEE_FUND, 1 billón de yoctoNEAR debería ser suficiente para 500 mil millones de transacciones, que debería ser lo suficientemente grande para el grupo de apuestas para que no se pueda recargar porque las recompensas se redistribuirán inmediatamente al total_stake_balance en el próximo ping. Dedicamos bastante tiempo y esfuerzo a este contrato, porque hicimos toneladas de revisiones de seguridad, incluso internas y externas, especialmente en torno a estas matemáticas. Eso fue complicado, y se descubrieron algunas cosas como la clave ristretto que apareció durante las revisiones. Marcamos el registro de cambios de este contrato, y en el archivo readme hay un montón de cosas que aparecieron durante el desarrollo y las pruebas en el sistema en vivo, pero la versión original tardó aproximadamente una semana en escribirse. Luego lo limpiamos, lo probamos y lo mejoramos. Luego hicimos un montón de revisiones. El grupo pidió las funciones de pausar y reanudar, porque de lo contrario el propietario no tenía la capacidad de desbloquearse si su nodo falla, o si atacaran la red. Básicamente, esta participación activa estaría solicitando la validación y no ejecutando la red. Solíamos no tener recortes de recompensas. Esto no era un problema para los participantes, pero era un problema para la propia red. De esa manera, el propietario puede pausar el staking, si no quiere ejecutar el grupo, o quiere migrar el grupo y comunicarse tanto como sea posible antes de esto. A continuación, actualizamos la interfaz de votación para que coincida con el contrato de votación de la fase dos final. Agregamos métodos de ayuda de vista para poder consultar cuentas de una manera legible por humanos. Finalmente, hubo algunas mejoras en torno a los métodos de procesamiento por lotes, de modo que las funciones deposit_and_stake, stake_all, unstake_all y withdraw_all en lugar de tener que hacer una llamada de vista primero, obtienen la cantidad y ponen la cantidad para llamar al bloqueo. Así es como lo arreglamos.

Cuando bloqueas, no solo bloqueas la cantidad, también adjuntamos una promesa para verificar si el bloqueo fue exitoso. Es necesario para dos cosas: tu, si estás tratando de bloquear con una clave no válida (no una clave específica de ristretto), la promesa fallará antes de la ejecución. Fallará la validación antes de enviarlo, y eso hará que no necesite verificarlo dentro del contrato. Revertirá la última llamada y todo irá bien. También introdujimos el bloqueo mínimo a nivel de protocolo. El bloqueo mínimo es una décima parte de la cantidad del precio del último asiento, y si tu contrato intenta bloquear menos que esto, la acción fallará y no enviará la promesa. Supongamos que quieres deshacer una cantidad y reduces tu saldo bloqueado por debajo de una décima parte de la cantidad bloqueada. La acción de bloqueo puede fallar, y tu no perderás la cantidad, mientras lo necesites para garantizar que tiene que suceder. En este caso, tenemos esta devolución de llamada que verifica que la acción de replanteo se haya completado con éxito. Esta devolución de llamada básicamente verifica que si falla y el saldo es positivo, debemos deshacernos de la apuesta. Por lo tanto, llamará a unstake para una acción en la que el monto bloqueado sea cero para asegurarse de que se libere todo el saldo. Puede retirarse en 4 epoch durante la prueba de estos contratos que hicimos en la red de prueba (testnet) beta 9 antes del mantenimiento. El contrato estaba listo tal vez alrededor del horario de verano, por lo que la prueba de esta iteración tomó probablemente de 2 a 4 meses debido a la complejidad que implica interactuar con el protocolo. Se aprendió mucho desde la paginación hasta los métodos auxiliares y se agruparon algunas cosas. Una cosa que sería realmente agradable tener sería la opción de bloquear o desbloquear y bloquear todo en un contrato de lockup. En este momento, debes emitir manualmente cuánto deseas bloquear en un contrato de lockup, pero sería genial si no necesitarás pensar en tu yoctoNEAR y cuánto estás bloqueado para almacenamiento. Solo quieres apostar todo desde tu bloqueo, pero como ya estaba implementado, era demasiado tarde para pensar en esto. También hay algo de gas que está hardcodeado, y con la disminución común en la tarifa, estos números no se pueden cambiar porque ya están en la cadena.

Por lo tanto, votar no es importante, pero el método ON_STAKE_ACTION_GAS requiere que tengas un gran número por cada bloqueo y no puedes disminuirlo. El riesgo es que en cada llamada de este contrato requerirá que tengamos una gran cantidad de gas y esto es un desperdicio. Supongamos que estamos de acuerdo en quemar todo el gas, entonces este gas siempre se quemaría y se desperdiciaría, pero además esto limita la cantidad de transacciones que puedes colocar en un bloque si estamos restringiendo el gas basados en este caso. Hubo mucha iteración en la prueba del contrato utilizando el marco de prueba de simulación que mejoramos mucho. Si llegamos a los contratos “lockup”, eventualmente, podrás ver cómo la estructura de los contratos de “lockup” mejoró con respecto a este.

18
Ir arriba